[comp.sys.mac.programmer] An itsy bitsy question...

aberno@questor.wimsey.bc.ca (Anthony Berno) (01/07/91)

I was wondering about something, partly from curiosity and partly because 
I am thinking of using it myself...

How is the "rubberbanding" done in programs like Superpaint when you are 
drawing a line on the screen? There isn't an obvious solution in Inside
Macintosh, and any solutions I have come up with are hideously difficult.
There must be a relatively simple way of doing it. Does anyone out there
know?

Thanks.
Anthony Berno.

mlab2@kuhub.cc.ukans.edu (01/07/91)

In article <La6cV1w164w@questor.wimsey.bc.ca>, aberno@questor.wimsey.bc.ca (Anthony Berno) writes:
> I was wondering about something, partly from curiosity and partly because 
> I am thinking of using it myself...
> 
> How is the "rubberbanding" done in programs like Superpaint when you are 
> drawing a line on the screen? There isn't an obvious solution in Inside
> Macintosh, and any solutions I have come up with are hideously difficult.
> There must be a relatively simple way of doing it. Does anyone out there
> know?
> 
> Thanks.
> Anthony Berno.

When in line-drawing-mode, a mouseDown event in the drawing window should call
a procedure similar to the below:

procedure RubberBand;
  var
    originalPt, lastPt, newPt : Point;
begin
  GetMouse(originalPoint);
  PenMode(patXOr);
  GetMouse(lastPt);
  MoveTo(originalPt.h, originalPt.v);
  LineTo(lastPt.h, lastPt.v);
  repeat
    MoveTo(originalPt.h, originalPt.v);
    LineTo(lastPt.h, lastPt);
    GetMouse(newPt);
    MoveTo(originalPt.h,originalPt.v);
    LineTo(newPt.h, newPt.v);
    lastPt:=newPt;
  until (not Button);
  PenNormal;
end; 

john calhoun

urlichs@smurf.sub.org (Matthias Urlichs) (01/08/91)

In comp.sys.mac.programmer, article <27725.2787e3d0@kuhub.cc.ukans.edu>,
  mlab2@kuhub.cc.ukans.edu writes:
< 
< When in line-drawing-mode, a mouseDown event in the drawing window should call
< a procedure similar to the below:
< 
Modify at will for drawing other shapes, of course...

I'd suggest some enhancements:
- drawing the rubberbanded line in gray
  (QD pattern for monochrome Macs, real gray for Color QD)
- drawing only when the mouse position has changed
- autoscrolling the new endpoint into the window
- invalidating the old line rather than simply erasing it;
  there might be other shapes behind it.

This will make the routine slightly more complicated, of course, but all the
add-ons should already be present in your program anyway. ;-)

< procedure RubberBand;
<   var
<     originalPt, lastPt, newPt : Point;
< begin
<   GetMouse(originalPoint);
<   PenMode(patXOr);
    PenPat(gray);
    (* you might also want to make the pen bigger, maybe one point bigger
       than the line you're drawing, so that it doesn't tend to vanish
       for near-45 degree angles *)
<   GetMouse(lastPt);
<   MoveTo(originalPt.h, originalPt.v);
<   LineTo(lastPt.h, lastPt.v);
<   repeat
     GetMouse(newPt);
     if newPt <> lastPt then begin
<     MoveTo(originalPt.h, originalPt.v);
<     LineTo(lastPt.h, lastPt);
<     (* GetMouse(newPt); *)
      ScrollIntoView(newPt);
      UpdateWindow;
<     MoveTo(originalPt.h,originalPt.v);
<     LineTo(newPt.h, newPt.v);
<     lastPt:=newPt;
     end;
<   until (not Button);
<   PenNormal;
    PenPat(black);
    (* reset pen size? *)
    MoveTo(originalPt.h, originalPt.v);
    LineTo(lastPt.h, lastPt.v);
    (* now erase and invalidate the location of the old line as there might
       have been bits behind it, probably by calling Pt2Rect and InvalRect *)
< end; 

-- 
Matthias Urlichs -- urlichs@smurf.sub.org -- urlichs@smurf.ira.uka.de     /(o\
Humboldtstrasse 7 - 7500 Karlsruhe 1 - FRG -- +49+721+621127(0700-2330)   \o)/

hawley@adobe.COM (Steve Hawley) (01/08/91)

In article <27725.2787e3d0@kuhub.cc.ukans.edu> mlab2@kuhub.cc.ukans.edu writes:
>In article <La6cV1w164w@questor.wimsey.bc.ca>, aberno@questor.wimsey.bc.ca (Anthony Berno) writes:
|> How is the "rubberbanding" done in programs like Superpaint when you are 
|> drawing a line on the screen?
|> Anthony Berno.
|
|When in line-drawing-mode, a mouseDown event in the drawing window should call
|a procedure similar to the below:
|procedure RubberBand;
|  var
|    originalPt, lastPt, newPt : Point;
|begin
|  GetMouse(originalPoint);
|  PenMode(patXOr);
|  GetMouse(lastPt);
|  MoveTo(originalPt.h, originalPt.v);
|  LineTo(lastPt.h, lastPt.v);
|  repeat
|    MoveTo(originalPt.h, originalPt.v);
|    LineTo(lastPt.h, lastPt);
|    GetMouse(newPt);
|    MoveTo(originalPt.h,originalPt.v);
|    LineTo(newPt.h, newPt.v);
|    lastPt:=newPt;
|  until (not Button);
|  PenNormal;
|end; 

Problem:
1) If the mouse doesn't move, the line will be flickering very badly.
2) The MoveTo/LineTo pairs are separated by a GetMouse() which will increase
   the flicker.
3) Erasing the old line first increases the "apparent" flicker
4) The loop should be terminated by the StillDown() function, not Button()

Solution:

  repeat
    GetMouse(newPt);
    MoveTo(originalPt.h, originalPt.v);    { Do the new point first }
    LineTo(newPt.h, newPt.v);              { This reduces apparent flicker }
    MoveTo(originalPt.h, originalPt.v);
    LineTo(lastPt.h, lastPt.v);
    lastPt := newPt;
  until (not StillDown(...I forget the args...));

Problem:
1) In such a tight loop, the overhead of the trap dispatcher is significant
   and contributes to the flicker in a bad way.

Solution:
Get the real function address from the GrafPorts QD procs in variables and
execute them directly

Problem:
1) When the new point differs from the old point by only a small amount, the
   line will still flicker because of the large amount of common area between
   the two lines, ie, drawing the new line will XOR over a large part of the
   old line leaving a big blank spot until the old line is re-drawn.

Solution:
   Copy window contents into offscreen bitmap #1 (a paint program would have
     this as the actual image data)
   SetPort to your own GrafPort that you have already setup with offscreen
     bitmap #2 as its portBits
   Hide the cursor

   repeat
      Get the new mouse coordinates
      CopyBits the smallest rectangle that encloses the old line, the new line
        and the cursor from bitmap #1 to bitmap #2
      Draw the new line in your port
      CopyBits the cursor mask onto bitmap #2
      CopyBits the cursor data onto bitmap #2
      CopyBits the smallest rectangle that encloses the old line, the new line
        and the cursor from bitmap #2 to the window
   until (not StillDown(...));

   Show the cursor

Can you say "Yuck"?  I knew you could...

The final method will give a minimal amount of flicker.  The problem is that
moving all that data around gets to be expensive in terms of response,
escpecially on bit lines.  For bonus points, you'd want the bitmaps to be
word or long word aligned to speed up the transfer, and you'd want the last
CopyBits (to the screen) to be in sync with the VBL rate.

The method that uses the pre-fetched addresses for the functions is probably
suitable for most needs, although probably not in a professional product.

Good luck...

Steve Hawley
hawley@adobe.com
Disclaimer: Adobe pays me to hack printers, not Macs.  I hack Macs in my spare
time
-- 
"I'm sick and tired of being told that ordinary decent people are fed up with
being sick and tired.  I know I'm certainly not, and I'm sick and tired of
begin told that I am." -Monty Python

jmunkki@hila.hut.fi (Juri Munkki) (01/08/91)

In article <27725.2787e3d0@kuhub.cc.ukans.edu> mlab2@kuhub.cc.ukans.edu writes:
>In article <La6cV1w164w@questor.wimsey.bc.ca>, aberno@questor.wimsey.bc.ca (Anthony Berno) writes:
>> How is the "rubberbanding" done in programs like Superpaint when you are 
>> drawing a line on the screen? There isn't an obvious solution in Inside
>> Macintosh, and any solutions I have come up with are hideously difficult.
>> There must be a relatively simple way of doing it. Does anyone out there
>> know?
>> 
>procedure RubberBand;
>  var
>    originalPt, lastPt, newPt : Point;
>begin
>  GetMouse(originalPoint);
>  PenMode(patXOr);
>  GetMouse(lastPt);
>  MoveTo(originalPt.h, originalPt.v);
>  LineTo(lastPt.h, lastPt.v);
>  repeat
>    MoveTo(originalPt.h, originalPt.v);
>    LineTo(lastPt.h, lastPt);
>    GetMouse(newPt);
>    MoveTo(originalPt.h,originalPt.v);
>    LineTo(newPt.h, newPt.v);
>    lastPt:=newPt;
>  until (not Button);
>  PenNormal;
>end; 

Don't do this. I can point out several faults in the above code. First of
all, it creates a quickly flickering line that can be invisible to the
user, if the timing happens to be right. Using exclusive or mode should
be ok in non-paint programs, but inverting a line when the user is trying
to paint a line is not good. Using an offscreen bitmap to restore the
background will look much nicer. Superpaint certainly doesn't use XOR
mode.

Most importantly, even loops like this one should use GetNextEvent.
GetNextEvent will tell you the mouse position as well as the mouse
button state. The nice thing about it is that it can keep track
of button events better than your loop. Your loop fails, if the
user rapidly releases and presses the mouse button. If you have
more than just two lines drawn in the loop, it's quite probable
that this will happen. This also means that you never find out
where the mouse button really came up. Moving the mouse quickly
while releasing the button will get you this result.

I also discovered that the GetMouse/Button approach doesn't really
work with QuicKeys (or at least the version QuicKeys that I have).

Here's what I'm using in Dizzy (a digital circuit simulator that
compiles for Think C and X/Motif on unix machines. It will be soon
released as free software WITH SOURCE CODE.):

/*
>>	Gets the mouse position while in a tracking loop.
>>	Returns true when the mouse button comes up.
*/
int GetMouseTrackEvent(pt)
Point	*pt;
{
	int 	r=0;
	
	r=GetNextEvent(mUpMask+mDownMask,&MyEvent);
	*pt=MyEvent.where;
	GlobalToLocal(pt);
	
	if(r)	r=(MyEvent.what==mouseUp);
	return !r;
}

You also have to check if the mouse moved, if you want to avoid flicker.
Of course this is easy to do with just an extra variable:

	do	/*	Loop until button is released.		*/
	{	downflag = GetMouseTrackEvent(&MousePoint);
		if(MousePoint.h!=OldSpot.h || MousePoint.v !=OldSpot.v)
		{	/* Ok, mouse moved, do the rubberbanding...
			blaa blaa...
			OldSpot = MousePoint;
		}
	}	while(downflag);

I use XOR mode in dizzy, but that's because it has rubberbanding operations
that do not need to be wysiwyg. The interesting thing about loops like
the one above is that they will work with X toolkit just as well, if you
rewrite GetMouseTrackEvent.

As a bonus, this kind of loop allows for multifinder background operations.
If you have terminal program in the background, it will still be able to run
while you rubberband. If you want absolute multifinder compatibility, you
just rewrite GetMouseTrackEvent to use WaitNextEvent. (Sorry, I don't have
a version like that... The only multifinder documents I have are in some
issue of Byte. I wonder why Apple doesn't make stuff like this available
in bookstores or at least technotes.)

   ____________________________________________________________________________
  / Juri Munkki	    /  Helsinki University of Technology   /  Wind  / Project /
 / jmunkki@hut.fi  /  Computing Center Macintosh Support  /  Surf  /  STORM  /
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

laf@mbunix.mitre.org (Lee Fyock) (01/08/91)

I've found that an easier way to avoid flicker (easier than using
background bitmaps, etc.) is to just put in a check to see if the
mouse has moved before erasing and redrawing the line.  This is
extremely flicker-free, with very little code.  It isn't, of course,
very multifinder friendly.  What happens to the GetNextEvent version
of this code if the user clicks on the multifinder icon in the
menu bar and the program gets switched out?  It won't know it
because app4events aren't being read, right?

Thanks,
Lee Fyock
laf@mbunix.mitre.org

d88-jwa@dront.nada.kth.se (Jon W{tte) (01/09/91)

In article <9700@adobe.UUCP> hawley@adobe.UUCP (Steve Hawley) writes:
>In article <27725.2787e3d0@kuhub.cc.ukans.edu> mlab2@kuhub.cc.ukans.edu writes:
>>In article <La6cV1w164w@questor.wimsey.bc.ca>, aberno@questor.wimsey.bc.ca (Anthony Berno) writes:
>|> How is the "rubberbanding" done in programs like Superpaint when you are 
>|> drawing a line on the screen?

>|When in line-drawing-mode, a mouseDown event in the drawing window
>|should call a procedure similar to the below:

>|  GetMouse(originalPoint);

This is not a good way to do it, You should assign originalPoint from
the where field of the event !

>Problem:
>1) If the mouse doesn't move, the line will be flickering very badly.
Yup !

>3) Erasing the old line first increases the "apparent" flicker
Yup...

>4) The loop should be terminated by the StillDown() function, not Button()
Yup !

>Solution:
>
>  repeat
>  until (not StillDown(...I forget the args...));

No args...

>Problem:
>1) In such a tight loop, the overhead of the trap dispatcher is significant
>   and contributes to the flicker in a bad way.
>Solution:
>Get the real function address from the GrafPorts QD procs in variables and
>execute them directly

NO ! This is NOT a recommended way anymore !

>Problem:
>1) When the new point differs from the old point by only a small amount, the
>   line will still flicker because of the large amount of common area between
>Solution:
>   Copy window contents into offscreen bitmap #1 (a paint program would have
>     this as the actual image data)

[ lots of bitmap stuff deleted ]

>Can you say "Yuck"?  I knew you could...

Fortunately, the solution is MUCH simpler !

>The method that uses the pre-fetched addresses for the functions is probably
>suitable for most needs, although probably not in a professional product.

It may break some exotic systems, too (like, the 8.24GC maybe ? Or some
other stuff ? Don't know about A/UX, for example)

>Good luck...

I recommend the following function:

Point
RubberBand ( Point start )
{
	Point end , tmp ;

	PenPat ( gray ) ;
	PenMode ( patXor ) ;
	PenSize ( 1 , 1 ) ;
	GetMouse ( & end ) ;
	if ( ! StillDown ( ) )
		return start ;
	MoveTo ( start . h , start . v ) ;
	LineTo ( end . h , end . v ) ;

	while ( StillDown ( ) ) {
		tmp = end ;
		GetMouse ( & end ) ;
		if ( end . v != tmp . v || end . h != tmp . h ) {
			LineTo ( start . h , start . v ) ;
			LineTo ( end . h , end . v ) ;
		}
	}

	return end ;
}

This routine looks very nice. One might argue that you should draw
the new line first and erase the old later, but that's a matter of
taste. Try it and see what you like best. Change the innermost
LineTos to:

	MoveTo ( end . h , end . v ) ;
	LineTo ( start . h , start . v ) ;
	LineTo ( tmp . h , tmp . v ) ;

The base of this approach is not to draw anything unless the pointer
has moved, and then only redraw what's necessary once.

No messing with offscreen bitmaps, and no messing with JSRs to
long ints (shudder) or call of function pointers. Any problem can
be solved easily and elegantly using some pure thought ;-)

Happy hacking,

							H+

Jon W{tte, Stockholm, Sweden, h+@nada.kth.se

jmunkki@hila.hut.fi (Juri Munkki) (01/09/91)

In article <127611@linus.mitre.org> laf@mbunix.mitre.org (Lee Fyock) writes:
>I've found that an easier way to avoid flicker (easier than using
>background bitmaps, etc.) is to just put in a check to see if the
>mouse has moved before erasing and redrawing the line.  This is
>extremely flicker-free, with very little code.

I suggested using offscreen bitmaps so that the exclusive or mode
could be avoided. It also eliminates flicker, that is just a side
effect and IMHO, no drawing should be made until it is necessary.
If you look at the code that I included, you will see that it
checks if the mouse has moved and only then draws.

>It isn't, of course,
>very multifinder friendly.  What happens to the GetNextEvent version
>of this code if the user clicks on the multifinder icon in the
>menu bar and the program gets switched out?  It won't know it
>because app4events aren't being read, right?

The GetNextEvent in my code is only called when mouse button is
down. It has the mouseDown mask because it is possible that the
mouseUp is lost in some conditions (like when you drop into Think
C's source debugger). If a mouseup is lost, a new mouseup will
not be reported before a mousedown is delivered. All other events
are masked out. This doesn't mean that the events are simply lost.
They should be quite safe in the event queue.

The application layer can not be changed by the user, because you need
to release the mouse button before you can CLICK the menu bar. When the
button is released, our rubberbanding loop exits and the normal main
even loop is probably used for further events. The only case where
a layer change might occur is if some external program (like the
debugger) causes the change. As far as I know, Apple User Interface
Guidelines explicitly forbid behavior like this.

Using StillDown to track the loop is extremely unfriendly to other
multifinder applications. A terminal program that relies on being
called regularly might fail a download just because the user is
holding down the mouse button. (Or maybe the mouse button is pressed
down by an object that is "temporarily stored on the mouse.")

Let's put a stop to programs that fail to allow background tasking
less than 10 times per second. The only reason why an application
will not allow background tasks should be when the user is selecting
from a menu (a menu manager menu).

I hope this answers your doubts about my programming techniques.

   ____________________________________________________________________________
  / Juri Munkki	    /  Helsinki University of Technology   /  Wind  / Project /
 / jmunkki@hut.fi  /  Computing Center Macintosh Support  /  Surf  /  STORM  /
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

sfleming@cs.hw.ac.uk (Stewart T. Fleming) (01/10/91)

In article <hf"2h2."b2@smurf.sub.org>, urlichs@smurf.sub.org (Matthias Urlichs) writes:
|> In comp.sys.mac.programmer, article <27725.2787e3d0@kuhub.cc.ukans.edu>,
|>   mlab2@kuhub.cc.ukans.edu writes:
|> < 
|> < When in line-drawing-mode, a mouseDown event in the drawing window should 
|> Modify at will for drawing other shapes, of course...
|> 

If you simply extend this procedure to do other shapes, then you will likely get horrific flicker problems.  True/False ?  Drawing and erasing the whole shape is fairly obvious to the user.  Drawing lines with this method is fine, but for other shapes :

Procedure TrackShape(Var currPt, prevPt, startPt: Point);

Var
	currRect, prevRect: Rect;
	prevRgn, dRgn: RgnHandle;

Begin
{Draw if mouse moves or is outside the window frame (autoscroll)}
If NeedToScroll(currPt) Or Not EqualPt(currPt, prevPt) Then
	Begin
	GetWindowFrame(currRect);	{Pin point in frame}
	PinInRect(currRect, currPt);

	Pt2Rect(startPt, currPt, currRect);	{Make shapes}
	Pt2Rect(startPt, prevPt, prevRect);

	dRgn := OutlineRgn(currRect);		{Outline shapes}
	prevRgn := OutlineRgn(prevRect);

	XorRgn(dRgn, prevRgn, gUtilRgn);	{Find bits changed}

	DisposeRgn(prevRgn);
	DisposeRgn(dRgn);

	PenMode(patXor);			{Draw bits changed}
	PaintRgn(gUtilRgn);
	End;
End;

Function OutlineRgn (theRect: Rect): RgnHandle;

Var
outRgn, inRgn: RgnHandle;

Begin
outRgn := NewRgn;
inRgn := NewRgn;

RectRgn(outRgn, theRect);	{ Construct region consisting of a	}
CopyRgn(outRgn, inRgn);		{   one pixel thick outline of		}
InsetRgn(inRgn, 1, 1);		{   the previous selection rect	}
DiffRgn(outRgn, inRgn, outRgn);

OutlineRgn := outRgn;
DisposeRgn(inRgn);
End;

I wrote this as a mouse task for Think Pascal with OOP.  It works, but I don't think it's too pretty.  Feel free to take it apart :-)

Here, I just use rectangles, but any shape could be done in the same way by changing OutlineRgn.  NeedToScroll checks if the point is outside the frame; if so, it calls ScrollRect.

|> I'd suggest some enhancements:
> - invalidating the old line rather than simply erasing it;
|>   there might be other shapes behind it.

Do you need to do this after every move ?  Or not until the user releases the mouse ?

|> Matthias Urlichs -- urlichs@smurf.sub.org -- urlichs@smurf.ira.uka.de     

Stewart
-- 
sfleming@cs.hw.ac.uk                                ...ukc!cs.hw.ac.uk!sfleming
"Before starting any programming project, try explaining it to your cat."