ftp.nice.ch/pub/next/science/mathematics/HippoDraw.2.0.s.tar.gz#/HippoDraw/Hippo.bproj/Draw.subproj/Graphic.m

This is Graphic.m in view mode; [Download] [Up]

#import "draw.h"
#import <math.h>

@implementation Graphic : Object

static int KNOB_WIDTH = 0.0;
static int KNOB_HEIGHT = 0.0;

#define MINSIZE 5.0	/* minimum size of a Graphic */

/* Object Versions */
#define NS30_VERSION 3
#define ROTATED_VERSION 4
#define CURRENT_VERSION ROTATED_VERSION

#define PI 3.1415926535

NXCursor *CrossCursor = nil;	/* global since subclassers may need it */

/* Optimization method. */

/*
 * The fastKnobFill optimization just keeps a list of black and dark gray
 * rectangles (the knobbies are made out of black and dark gray rectangles)
 * and emits them in a single NXRectFillList() which is much faster than
 * doing individual rectfills (we also save the repeated setgrays).
 */

static NXRect *blackRectList = NULL;
static int blackRectSize = 0;
static int blackRectCount = 0;
static NXRect *dkgrayRectList = NULL;
static int dkgrayRectSize = 0;
static int dkgrayRectCount = 0;

+ fastKnobFill:(const NXRect *)aRect isBlack:(BOOL)isBlack
{
    if (!aRect) return self;

    if (isBlack) {
	if (!blackRectList) {
	    blackRectSize = 16;
	    NX_ZONEMALLOC([NXApp zone], blackRectList, NXRect, blackRectSize);
	} else {
	    while (blackRectCount >= blackRectSize) blackRectSize <<= 1;
	    NX_ZONEREALLOC([NXApp zone], blackRectList, NXRect, blackRectSize);
	}
	blackRectList[blackRectCount++] = *aRect;
    } else {
	if (!dkgrayRectList) {
	    dkgrayRectSize = 16;
	    NX_ZONEMALLOC([NXApp zone], dkgrayRectList, NXRect, dkgrayRectSize);
	} else {
	    while (dkgrayRectCount >= dkgrayRectSize) dkgrayRectSize <<= 1;
	    NX_ZONEREALLOC([NXApp zone], dkgrayRectList, NXRect, dkgrayRectSize);
	}
	dkgrayRectList[dkgrayRectCount++] = *aRect;
    }

    return self;
}

+ showFastKnobFills
{
    PSsetgray(NX_BLACK);
    NXRectFillList(blackRectList, blackRectCount);
    PSsetgray(NX_DKGRAY);
    NXRectFillList(dkgrayRectList, dkgrayRectCount);
    blackRectCount = 0;
    dkgrayRectCount = 0;
    return self;
}

/* Factory methods. */

+ initialize
/*
 * This sets the class version so that we can compatibly read
 * old Graphic objects out of an archive.
 */
{
    [Graphic setVersion:CURRENT_VERSION];
    return self;
}

+ (BOOL)isEditable
/*
 * Any Graphic which can be edited should return YES from this
 * and its instances should do something in the response to the
 * edit:in: method.
 */
{
    return NO;
}

+ cursor
/*
 * Any Graphic that doesn't have a special cursor gets the default cross.
 * Since Draw has been converted to subproject in Hippo.bundle,
 * we have to find the tiff files. -- pfkeb
 */
{
    NXBundle	*bundle;
    NXImage	*image;
    NXPoint 	spot;
    char	buffer[MAXPATHLEN+1];

    if (!CrossCursor) {
        bundle = [NXBundle bundleForClass:[self class]];
        if ( [bundle getPath:buffer forResource:"cross" ofType:"tiff"] ) {
	    image = [[NXImage allocFromZone:[self zone]] initFromFile:buffer];
	    CrossCursor = [NXCursor newFromImage:image];
	    spot.x = 7.0; spot.y = 7.0;
	    [CrossCursor setHotSpot:&spot];
	}
    }

    return CrossCursor;
}

static void initClassVars()
{
    const char *value;
    NXCoord w = 2.0, h = 2.0;

    if (!KNOB_WIDTH) {
	value = NXGetDefaultValue([NXApp appName], "KnobWidth");
	if (value) w = floor(atof(value) / 2.0);
	value = NXGetDefaultValue([NXApp appName], "KnobHeight");
	if (value) h = floor(atof(value) / 2.0);
	w = MAX(w, 1.0); h = MAX(h, 1.0);
	KNOB_WIDTH = w * 2.0 + 1.0;	/* size must be odd */
	KNOB_HEIGHT = h * 2.0 + 1.0;
    }
}

/*
 * The currentGraphicIdentifier is a number that is kept unique for a given
 * Draw document by being monotonically increasing and is bumped each time a
 * new Graphic is created.  The method of the same name is used during the
 * archiving of a Draw document to write out what the number is at save-time.
 * updateCurrentGraphicIdentifer: is used at document load time to reset
 * the number to that level (if it's already higher, then we don't need to
 * bump it).
 */

static int currentGraphicIdentifier = 1;

+ (int)currentGraphicIdentifier
{
    return currentGraphicIdentifier;
}

+ updateCurrentGraphicIdentifier:(int)newMaxIdentifier
{
    if (newMaxIdentifier > currentGraphicIdentifier) currentGraphicIdentifier = newMaxIdentifier;
    return self;
}

- init
{
    [super init];
    gFlags.active = YES;
    gFlags.selected = YES;
    initClassVars();
    identifier = currentGraphicIdentifier++;
    return self;
}

- awake
{
    initClassVars();
    return [super awake];
}

/* Private C functions and macros used to implement methods in this class. */

static void drawKnobs(const NXRect *rect, int cornerMask, BOOL black)
/*
 * Draws either the knobs or their shadows (not both).
 */
{
    NXRect knob;
    NXCoord dx, dy;
    BOOL oddx, oddy;

    knob = *rect;
    dx = knob.size.width / 2.0;
    dy = knob.size.height / 2.0;
    oddx = (floor(dx) != dx);
    oddy = (floor(dy) != dy);
    knob.size.width = KNOB_WIDTH;
    knob.size.height = KNOB_HEIGHT;
    knob.origin.x -= ((KNOB_WIDTH - 1.0) / 2.0);
    knob.origin.y -= ((KNOB_HEIGHT - 1.0) / 2.0);

    if (cornerMask & LOWER_LEFT_MASK) [Graphic fastKnobFill:&knob isBlack:black];
    knob.origin.y += dy;
    if (oddy) knob.origin.y -= 0.5;
    if (cornerMask & LEFT_SIDE_MASK) [Graphic fastKnobFill:&knob isBlack:black];
    knob.origin.y += dy;
    if (oddy) knob.origin.y += 0.5;
    if (cornerMask & UPPER_LEFT_MASK) [Graphic fastKnobFill:&knob isBlack:black];
    knob.origin.x += dx;
    if (oddx) knob.origin.x -= 0.5;
    if (cornerMask & TOP_SIDE_MASK) [Graphic fastKnobFill:&knob isBlack:black];
    knob.origin.x += dx;
    if (oddx) knob.origin.x += 0.5;
    if (cornerMask & UPPER_RIGHT_MASK) [Graphic fastKnobFill:&knob isBlack:black];
    knob.origin.y -= dy;
    if (oddy) knob.origin.y -= 0.5;
    if (cornerMask & RIGHT_SIDE_MASK) [Graphic fastKnobFill:&knob isBlack:black];
    knob.origin.y -= dy;
    if (oddy) knob.origin.y += 0.5;
    if (cornerMask & LOWER_RIGHT_MASK) [Graphic fastKnobFill:&knob isBlack:black];
    knob.origin.x -= dx;
    if (oddx) knob.origin.x += 0.5;
    if (cornerMask & BOTTOM_SIDE_MASK) [Graphic fastKnobFill:&knob isBlack:black];
}

static void unrotRect( NXRect *u, NXRect *b, NXPoint *p0, int angle );

/* Private methods sometimes overridden by subclassers */

- setGraphicsState
/*
 * Emits a gsave, must be balanced by grestore.
 */
{
    PSSetParameters(gFlags.linecap, gFlags.linejoin, linewidth);
    return self;
}

- setLineColor
{
    if (lineColor) {
	NXSetColor(*lineColor);
    } else {
	NXSetColor(NX_COLORBLACK);
    }
    return self;
}

- setFillColor
{
    if (fillColor) NXSetColor(*fillColor);
    return self;
}

- (int)cornerMask
/*
 * Returns a mask of the corners which should have a knobby in them.
 */
{
    return ALL_CORNERS;
}

/* Data link methods -- see Links.rtf and gvLinks.m for more info */

/*
 * Most Graphics aren't linked (i.e. their visual display is
 * not determined by some other document).  See Image and
 * TextGraphic for examples of Graphics that sometimes do.
 */

- setLink:(NXDataLink *)aLink
{
    return nil;
}

- (NXDataLink *)link
{
    return nil;
}

- (Graphic *)graphicLinkedBy:(NXDataLink *)aLink
/*
 * The reason we implement this method (instead of just relying on
 * saying if ([graphic link] == aLink)) is for the sake of Group
 * objects which may have a linked Graphic embedded in them.
 */
{
    NXDataLink *link = [self link];

    if (link) {
	if (!aLink) {	/* !aLink means any link */
	    return ([link disposition] != NX_LinkBroken) ? self : nil;
	} else {
	    return (aLink == link) ? self : nil;
	}
    }

    return nil;
}

- reviveLink:(NXDataLinkManager *)linkManager
/*
 * We never archive link information (but, of course, the unique identifer
 * is always archived with a Graphic).  Thus, when our document is reloaded,
 * we just asked the NXDataLinkManager which NXDataLink object is associated
 * with the NXSelection which represents this Graphic.
 */
{
    if (![self link]) [self setLink:[linkManager findDestinationLinkWithSelection:[self selection]]];
    return self;
}

- (NXSelection *)selection
/*
 * Just creates an NXSelection "bag o' bits" with our unique identifier in it.
 */
{
    char buffer[200];
    sprintf(buffer, "%d %d", ByGraphic, [self identifier]);
    return [[NXSelection allocFromZone:[self zone]] initWithDescription:buffer length:strlen(buffer)+1];
}

- (BOOL)mightBeLinked
/*
 * This is set whenever our Graphic has a link set in it.
 * It is never cleared.
 * We use it during copy/paste to determine whether we have
 * to check with the data link manager to possibly reestablish
 * a link to this object.
 */
{
    return gFlags.mightBeLinked;
}

/* Notification messages */

/*
 * These methods are sent when a Graphic is added to or removed
 * from a GraphicView (respectively).  Currently we only use them
 * to break and reestablish links if any.
 */

- wasAddedTo:(GraphicView *)sender
{
    NXDataLink *link;
    NXDataLinkManager *linkManager;

    if ((linkManager = [sender linkManager]) && (link = [self link])) {
	if ([link disposition] == NX_LinkBroken) {
	    [linkManager addLink:link at:[self selection]];
	}
    }

    return self;
}

- wasRemovedFrom:(GraphicView *)sender
{
    [[self link] break];
    return self;
}

/* Methods for uniquely identifying a Graphic. */

- resetIdentifier
{
    identifier = currentGraphicIdentifier++;
    return self;
}

- writeIdentifierTo:(char *)buffer
/*
 * This method is necessary to support a Group which never writes out
 * its own identifier, but, instead has its components each write out
 * their own identifier.
 */
{
    sprintf(buffer, "%d", identifier);
    return self;
}

- (int)identifier
{
    return identifier;
}

- (Graphic *)graphicIdentifiedBy:(int)anIdentifier
{
    return (identifier == anIdentifier) ? self : nil;
}

/* Event handling */

- (BOOL)handleEvent:(NXEvent *)event at:(const NXPoint *)p inView:(View *)view
/*
 * Currently the only Graphic's that handle events are Image Graphic's that
 * are linked to something else (they follow the link on double-click and
 * the track the mouse for link buttons, for example).  This method should
 * return YES only if it tracked the mouse until it went up.
 */
{
    return NO;
}

/* Number of Graphics this Graphic represents (always 1 for non-Group). */

- (int)graphicCount
/*
 * This is really only here to support Groups.  It is used by the
 * Object Link stuff purely to know how much space will be needed to
 * create an NXSelection with the unique identifiers of all the objects
 * in the selection.
 */
{
    return 1;
}

/* Public routines mostly called by GraphicView's. */

- (const char *)title
{
    return NXLoadLocalStringFromTableInBundle(NULL, nil, [self name], NULL);
}

- (BOOL)isSelected
{
    return gFlags.selected;
}

- (BOOL)isActive
{
    return gFlags.active;
}

- (BOOL)isLocked
{
    return gFlags.locked;
}

- select
{
    gFlags.selected = YES;
    return self;
}

- deselect
{
    gFlags.selected = NO;
    return self;
}

- activate
/*
 * Activation is used to *temporarily* take a Graphic out of the GraphicView.
 */
{
    gFlags.active = YES;
    return self;
}

- deactivate
{
    gFlags.active = NO;
    return self;
}

- lock
/*
 * A locked graphic cannot be selected, resized or moved.
 */
{
    gFlags.locked = YES;
    return self;
}

- unlock
{
    gFlags.locked = NO;
    return self;
}

/* See TextGraphic for more info about form entries. */

- (BOOL)isFormEntry
{
    return NO;
}

- setFormEntry:(int)flag
{
    return self;
}

- (BOOL)hasFormEntries
{
    return NO;
}

- (BOOL)writeFormEntryToStream:(NXStream *)stream
{
    return NO;
}

/* See Group and Image for more info about cacheability. */

- setCacheable:(BOOL)flag
{
    return self;
}

- (BOOL)isCacheable
{
    return YES;
}

/* Getting and setting the bounds. */

- getBounds:(NXRect *)theRect
{
    *theRect = bounds;
    return self;
}

- setBounds:(const NXRect *)aRect
{
    bounds = *aRect;
    return self;
}

- (NXRect *)getExtendedBounds:(NXRect *)theRect
/*
 * Returns, by reference, the rectangle which encloses the Graphic
 * AND ITS KNOBBIES and its increased line width (if appropriate).
 */
{
    if (bounds.size.width < 0.0) {
	theRect->origin.x = bounds.origin.x + bounds.size.width;
	theRect->size.width = - bounds.size.width;
    } else {
	theRect->origin.x = bounds.origin.x;
	theRect->size.width = bounds.size.width;
    }
    if (bounds.size.height < 0.0) {
	theRect->origin.y = bounds.origin.y + bounds.size.height;
	theRect->size.height = - bounds.size.height;
    } else {
	theRect->origin.y = bounds.origin.y;
	theRect->size.height = bounds.size.height;
    }

    theRect->size.width = MAX(1.0, theRect->size.width);
    theRect->size.height = MAX(1.0, theRect->size.height);

    NXInsetRect(theRect, - ((KNOB_WIDTH - 1.0) + linewidth + 1.0),
			 - ((KNOB_HEIGHT - 1.0) + linewidth + 1.0));

    if (gFlags.arrow) {
	if (linewidth) {
	    NXInsetRect(theRect, - linewidth * 2.5, - linewidth * 2.5);
	} else {
	    NXInsetRect(theRect, - 13.0, - 13.0);
	}
    }

    NXIntegralRect(theRect);    
    return theRect;
}

- (int)knobHit:(const NXPoint *)p
/*
 * Returns 0 if point is in bounds, and Graphic isOpaque, and no knobHit.
 * Returns -1 if outside bounds or not opaque or not active.
 * Returns corner number if there is a hit on a corner.
 * We have to be careful when the bounds are off an odd size since the
 * knobs on the sides are one pixel larger.
 */
{
    NXRect eb;
    NXRect knob;
    NXCoord dx, dy;
    BOOL oddx, oddy;
    int cornerMask = [self cornerMask];

    [self getExtendedBounds:&eb];

    if (!gFlags.active) {
	return -1;
    } else if (!gFlags.selected) {
        return (NXMouseInRect(p, &bounds, NO) && [self isOpaque]) ? 0 : -1;
    } else {
        if (!NXMouseInRect(p, &eb, NO)) return -1;
    }

    knob = bounds;
    dx = knob.size.width / 2.0;
    dy = knob.size.height / 2.0;
    oddx = (floor(dx) != dx);
    oddy = (floor(dy) != dy);
    knob.size.width = KNOB_WIDTH;
    knob.size.height = KNOB_HEIGHT;
    knob.origin.x -= ((KNOB_WIDTH - 1.0) / 2.0);
    knob.origin.y -= ((KNOB_HEIGHT - 1.0) / 2.0);

    if ((cornerMask & LOWER_LEFT_MASK) && NXMouseInRect(p, &knob, NO))
	return(LOWER_LEFT);
    knob.origin.y += dy;
    if (oddy) knob.origin.y -= 0.5;
    if ((cornerMask & LEFT_SIDE_MASK) && NXMouseInRect(p, &knob, NO))
	return(LEFT_SIDE);
    knob.origin.y += dy;
    if (oddy) knob.origin.y += 0.5;
    if ((cornerMask & UPPER_LEFT_MASK) && NXMouseInRect(p, &knob, NO))
	return(UPPER_LEFT);
    knob.origin.x += dx;
    if (oddx) knob.origin.x -= 0.5;
    if ((cornerMask & TOP_SIDE_MASK) && NXMouseInRect(p, &knob, NO))
	return(TOP_SIDE);
    knob.origin.x += dx;
    if (oddx) knob.origin.x += 0.5;
    if ((cornerMask & UPPER_RIGHT_MASK) && NXMouseInRect(p, &knob, NO))
	return(UPPER_RIGHT);
    knob.origin.y -= dy;
    if (oddy) knob.origin.y -= 0.5;
    if ((cornerMask & RIGHT_SIDE_MASK) && NXMouseInRect(p, &knob, NO))
	return(RIGHT_SIDE);
    knob.origin.y -= dy;
    if (oddy) knob.origin.y += 0.5;
    if ((cornerMask & LOWER_RIGHT_MASK) && NXMouseInRect(p, &knob, NO))
	return(LOWER_RIGHT);
    knob.origin.x -= dx;
    if (oddx) knob.origin.x += 0.5;
    if ((cornerMask & BOTTOM_SIDE_MASK) && NXMouseInRect(p, &knob, NO))
	return(BOTTOM_SIDE);

    return NXMouseInRect(p, &bounds, NO) ? ([self isOpaque] ? 0 : -1) : -1;
}

/* This method is analogous to display (not drawSelf::) in View. */

- draw:(const NXRect *)rect
/*
 * Draws the graphic inside rect.  If rect is NULL, then it draws the
 * entire Graphic.  If the Graphic is not intersected by rect, then it
 * is not drawn at all.  If the Graphic is selected, it is drawn with
 * its knobbies.  This method is not intended to be overridden.  It
 * calls the overrideable method "draw" which doesn't have to worry
 * about drawing the knobbies.
 *
 * Note the showFastKnobFills optimization here.  If this Graphic is
 * opaque then there is a possibility that it might obscure knobbies
 * of Graphics underneath it, so we must emit the cached rectfills
 * before drawing this Graphic.
 */
{
    NXRect              r;
    NXRect boundSave;

    [self getExtendedBounds:&r];
    if (gFlags.active && (!rect || NXIntersectsRect(rect, &r))) {
	if ([self isOpaque])
	    [Graphic showFastKnobFills];
	[self setGraphicsState];/* does a gsave */
 	if (rotAngle != 0.0) 
	{
	    boundSave = bounds;
	    unrotRect( &bounds, &boundSave, &unrotOrigin, rotAngle);
	    PStranslate(bounds.origin.x, bounds.origin.y);
	    PSrotate(rotAngle);
	    PStranslate(-bounds.origin.x, -bounds.origin.y);
	}
	[self draw];
	if (rotAngle != 0.0) bounds = boundSave;
	PSgrestore();		/* so we need a grestore here */
	if (NXDrawingStatus == NX_DRAWING) {
	    if (gFlags.selected) {
		r.origin.x = floor(bounds.origin.x);
		r.origin.y = floor(bounds.origin.y);
		r.size.width = floor(bounds.origin.x + bounds.size.width 
				     + 0.99) - r.origin.x;
		r.size.height = floor(bounds.origin.y + bounds.size.height 
		                      + 0.99) - r.origin.y;
		r.origin.x += 1.0;
		r.origin.y -= 1.0;
		drawKnobs(&r,[self cornerMask], YES);	/* shadows */
		r.origin.x = floor(bounds.origin.x);
		r.origin.y = floor(bounds.origin.y);
		r.size.width = floor(bounds.origin.x + bounds.size.width 
		                     + 0.99) - r.origin.x;
		r.size.height = floor(bounds.origin.y + bounds.size.height 
		                      + 0.99) - r.origin.y;
		drawKnobs(&r,[self cornerMask], NO);	/* knobs */
	    }
	}
	
	return self;
    }
    return nil;
}
/*
 * Returns whether this Graphic can emit, all by itself, fully
 * encapsulated PostScript (or fully conforming TIFF) representing
 * itself.  This is an optimization for copy/paste.
 */

- (BOOL)canEmitEPS
{
    return NO;
}

- (BOOL)canEmitTIFF
{
    return NO;
}

/* Sizing, aligning and moving. */

- moveLeftEdgeTo:(const NXCoord *)x
{
    bounds.origin.x = *x;
    return self;
}

- moveRightEdgeTo:(const NXCoord *)x
{
    bounds.origin.x = *x - bounds.size.width;
    return self;
}

- moveTopEdgeTo:(const NXCoord *)y
{
    bounds.origin.y = *y - bounds.size.height;
    return self;
}

- moveBottomEdgeTo:(const NXCoord *)y
{
    bounds.origin.y = *y;
    return self;
}

- moveHorizontalCenterTo:(const NXCoord *)x
{
    bounds.origin.x = *x - floor(bounds.size.width / 2.0);
    return self;
}

- moveVerticalCenterTo:(const NXCoord *)y
{
    bounds.origin.y = *y - floor(bounds.size.height / 2.0);
    return self;
}

- (NXCoord)baseline
{
    return 0.0;
}

- moveBaselineTo:(const NXCoord *)y
{
    return self;
}

- moveBy:(const NXPoint *)offset
{
    bounds.origin.x += floor(offset->x);
    bounds.origin.y += floor(offset->y);
    return self;
}

- moveTo:(const NXPoint *)p
{
    bounds.origin.x = floor(p->x);
    bounds.origin.y = floor(p->y);
    return self;
}

- centerAt:(const NXPoint *)p
{
    bounds.origin.x = floor(p->x - bounds.size.width / 2.0);
    bounds.origin.y = floor(p->y - bounds.size.height / 2.0);
    return self;
}

- sizeTo:(const NXSize *)size
{
    bounds.size.width = floor(size->width);
    bounds.size.height = floor(size->height);
    return self;
}

- sizeToNaturalAspectRatio
{
    return [self constrainCorner:UPPER_RIGHT toAspectRatio:[self naturalAspectRatio]];
}

- sizeToGrid:(GraphicView *)graphicView
{
    NXPoint p;

    [graphicView grid:&bounds.origin];
    p.x = bounds.origin.x + bounds.size.width;
    p.y = bounds.origin.y + bounds.size.height;
    [graphicView grid:&p];
    bounds.size.width = p.x - bounds.origin.x;
    bounds.size.height = p.y - bounds.origin.y;

    return self;
}

- alignToGrid:(GraphicView *)graphicView
{
    [graphicView grid:&bounds.origin];
    return self;
}

/* don't call this function with b == u */
static void unrotRect( NXRect *u, NXRect *b, NXPoint *p0, int angle )
{
    double a = angle * PI / 180.0;
    
    u->origin.x = p0->x * b->size.width;
    u->origin.y = p0->y * b->size.height;
    if (angle == 0 || angle == 180 || angle == -180)
    {
    	u->size.width = b->size.width;
	u->size.height = b->size.height;
    }
    else if (angle == 90 || angle == -90)
    {
    	u->size.width = b->size.height;
	u->size.height = b->size.width;
    }
    else if (angle > 0 && angle < 90) 
    {
    	u->size.width = (b->size.width - u->origin.x)/cos(a);
	u->size.height = u->origin.x / sin(a);
    }
    else if (angle > 90 && angle < 180) 
    {
    	u->size.width = (b->size.height - u->origin.y)/sin(a);
	u->size.height = -u->origin.y / cos(a);
    }
    else if (angle > -90 && angle < 0) 
    {
	u->size.width = -u->origin.y / sin(a);
    	u->size.height = (b->size.height - u->origin.y)/cos(a);
    }
    else
    {
	u->size.width = -u->origin.x / cos(a);
    	u->size.height = (-b->size.width + u->origin.x)/sin(a);
    }
    u->origin.x += b->origin.x;
    u->origin.y += b->origin.y;
}

- rotateTo:(const int *)angle
{
    NXRect temp;
    int a;
    
    if (*angle == rotAngle) return self;

    /*
     * make sure angle is in the useful range
     */
    a = *angle;
    while (a > 180) a -= 360;
    while (a < -180) a += 360;
    
    if (rotAngle == 0)
    	temp = bounds;
    else
	unrotRect( &temp, &bounds, &unrotOrigin, rotAngle);
    	
    rotAngle = a;
    if (rotAngle == 0)
    	bounds = temp;
    else
    {
        float ang = rotAngle * PI/180.0;
    	float x2 = temp.size.width * cos(ang);
	float y2 = temp.size.width * sin(ang);
	float x3 = x2 - temp.size.height * sin(ang);
	float y3 = y2 + temp.size.height * cos(ang);
	float x4 = - temp.size.height * sin(ang);
	float y4 = temp.size.height * cos(ang);
	bounds.origin.x = MIN(0.0, MIN(x2, MIN(x3, x4)));
	bounds.origin.y = MIN(0.0, MIN(y2, MIN(y3, y4)));
	bounds.size.width = MAX(0.0, MAX(x2, MAX(x3, x4))) - 
				bounds.origin.x;
	bounds.size.height = MAX(0.0, MAX(y2, MAX(y3, y4))) - 
				bounds.origin.y;
	bounds.origin.x += temp.origin.x;
	bounds.origin.y += temp.origin.y;
    }
    unrotOrigin.x = (temp.origin.x - bounds.origin.x)
    		/ bounds.size.width;
    unrotOrigin.y = (temp.origin.y - bounds.origin.y)
    		/ bounds.size.height;

    return self;
}

- rotateBy:(const int *) angle
{
    int t = rotAngle+*angle;
    return [self rotateTo: &t];
}

- (int) rotation
{
	return rotAngle;
}

/* Compatibility method for old PSGraphic and Tiff classes. */

- replaceWithImage
{
    return self;
}

/* Public routines. */

- setLineWidth:(const float *)value
/*
 * This is called with value indirected so that it can be called via
 * a perform:with: method.  Kind of screwy, but ...
 */
{
    if (value) linewidth = *value;
    return self;
}

- (float)lineWidth
{
    return linewidth;
}

- setLineColor:(const NXColor *)color
{
    if (color) {
	if (NXEqualColor(*color, NX_COLORBLACK)) {
	    NX_FREE(lineColor);
	    lineColor = NULL;
	    gFlags.nooutline = NO;
	} else {
	    if (!lineColor) NX_ZONEMALLOC([self zone], lineColor, NXColor, 1);
	    *lineColor = *color;
	    gFlags.nooutline = NO;
	}
    }
    return self;
}

- (NXColor)lineColor
{
    return lineColor ? *lineColor : NX_COLORBLACK;
}

- setFillColor:(const NXColor *)color
{
    if (color) {
	if (!fillColor) NX_ZONEMALLOC([self zone], fillColor, NXColor, 1);
	*fillColor = *color;
	if (![self fill]) [self setFill:FILL_NZWR];
    }
    return self;
}

- (NXColor)fillColor
{
    return fillColor ? *fillColor : NX_COLORWHITE;
}

- (Graphic *)colorAcceptorAt:(const NXPoint *)point
/*
 * This method supports dragging and dropping colors on Graphics.
 * Whatever object is returned from this may well be sent
 * setFillColor: if the color actually gets dropped on it.
 * See gvDrag.m's acceptsColor:atPoint: method.
 */
{
    return nil;
}

- changeFont:sender
{
    return self;
}

- font
{
    return nil;
}

- setGray:(const float *)value
/*
 * This is called with value indirected so that it can be called via
 * a perform:with: method.  Kind of screwy, but ...
 * Now that we have converted to using NXColor's, we'll interpret this
 * method as a request to set the lineColor.
 */
{
    NXColor color;

    if (value) {
	color = NXConvertGrayToColor(*value);
	[self setLineColor:&color];
    }

    return self;
}

- (float)gray
{
    float retval;

    if (lineColor) {
	NXConvertColorToGray(*lineColor, &retval);
    } else {
	retval = NX_BLACK;
    }

    return retval;
}

- setFill:(int)mode
{
    switch (mode) {
	case FILL_NONE:	gFlags.eofill = gFlags.fill = NO; break;
	case FILL_EO:	gFlags.eofill = YES; gFlags.fill = NO; break;
	case FILL_NZWR:	gFlags.eofill = NO; gFlags.fill = YES; break;
    }
    return self;
}

- (int)fill
{
    if (gFlags.eofill) {
	return FILL_EO;
    } else if (gFlags.fill) {
	return FILL_NZWR;
    } else {
	return FILL_NONE;
    }
}

- setOutlined:(BOOL)outlinedFlag
{
    gFlags.nooutline = outlinedFlag ? NO : YES;
    return self;
}

- (BOOL)isOutlined
{
    return gFlags.nooutline ? NO : YES;
}

- setLineCap:(int)capValue
{
    if (capValue >= 0 && capValue <= 2) {
	gFlags.linecap = capValue;
    }
    return self;
}

- (int)lineCap
{
    return gFlags.linecap;
}

- setLineArrow:(int)arrowValue
{
    if (arrowValue >= 0 && arrowValue <= 3) {
	gFlags.arrow = arrowValue;
    }
    return self;
}

- (int)lineArrow
{
    return gFlags.arrow;
}

- setLineJoin:(int)joinValue
{
    if (joinValue >= 0 && joinValue <= 2) {
	gFlags.linejoin = joinValue;
    }
    return self;
}

- (int)lineJoin
{
    return gFlags.linejoin;
}

/* Archiver-related methods. */

- write:(NXTypedStream *)stream
/*
 * Since a typical document has many Graphics, we want to try and make
 * the archived document small, so we don't write out the linewidth and
 * gray values if they are the most common 0 and NX_BLACK.  To accomplish
 * this, we note that we haven't written them out by setting the
 * bits in gFlags.
 */
{
    [super write:stream];
    gFlags.linewidthSet = (linewidth != 0.0);
    gFlags.lineColorSet = lineColor ? YES : NO;
    gFlags.fillColorSet = fillColor ? YES : NO;
    NXWriteTypes(stream, "ffffii", &bounds.origin.x, &bounds.origin.y,
	&bounds.size.width, &bounds.size.height, &gFlags, &identifier);
    if (gFlags.linewidthSet) NXWriteTypes(stream, "f", &linewidth);
    if (gFlags.lineColorSet) NXWriteColor(stream, *lineColor);
    if (gFlags.fillColorSet) NXWriteColor(stream, *fillColor);
    
    // new with ROTATED_VERSION 
    NXWriteTypes(stream, "f", &rotAngle);
    NXWriteTypes(stream, "ff", &unrotOrigin.x, &unrotOrigin.y );
    
    return self;
}

- read:(NXTypedStream *)stream
{
    int version;
    float gray = NX_BLACK;

    [super read:stream];
    version = NXTypedStreamClassVersion(stream, "Graphic");
    if (version > 2) {
	NXReadTypes(stream, "ffffii", &bounds.origin.x, &bounds.origin.y,
	    &bounds.size.width, &bounds.size.height, &gFlags, &identifier);
    } else if (version > 1) {
	NXReadTypes(stream, "ffffsi", &bounds.origin.x, &bounds.origin.y,
	    &bounds.size.width, &bounds.size.height, &gFlags, &identifier);
    } else {
	NXReadTypes(stream, "ffffs", &bounds.origin.x, &bounds.origin.y,
	    &bounds.size.width, &bounds.size.height, &gFlags);
	identifier = currentGraphicIdentifier++;
    }
    if (version > 1 && identifier >= currentGraphicIdentifier) currentGraphicIdentifier = identifier+1;
    if (gFlags.linewidthSet) NXReadTypes(stream, "f", &linewidth);
    if (version < 1) {
	if (gFlags.lineColorSet) NXReadTypes(stream, "f", &gray);
	if (gFlags.fillColorSet && (gFlags.eofill | gFlags.fill)) {
	    NX_ZONEMALLOC([self zone], lineColor, NXColor, 1);
	    *lineColor = NXConvertGrayToColor(NX_BLACK);
	    NX_ZONEMALLOC([self zone], fillColor, NXColor, 1);
	    *fillColor = NXConvertGrayToColor(gray);
	} else if (gFlags.eofill | gFlags.fill) {
	    NX_ZONEMALLOC([self zone], fillColor, NXColor, 1);
	    *fillColor = NXConvertGrayToColor(gray);
	    [self setOutlined:NO];
	} else {
	    NX_ZONEMALLOC([self zone], lineColor, NXColor, 1);
	    *lineColor = NXConvertGrayToColor(gray);
	}
    } else {
	if (gFlags.lineColorSet) {
	    NX_ZONEMALLOC([self zone], lineColor, NXColor, 1);
	    *lineColor = NXReadColor(stream);
	    if (NXEqualColor(*lineColor, NX_COLORCLEAR)) {
		free(lineColor);
		lineColor = NULL;
		[self setOutlined:NO];
	    }
	}
	if (gFlags.fillColorSet) {
	    NX_ZONEMALLOC([self zone], fillColor, NXColor, 1);
	    *fillColor = NXReadColor(stream);
	    if (NXEqualColor(*fillColor, NX_COLORCLEAR) || (NXAlphaComponent(*fillColor) == 0.0)) {
		free(fillColor);
		fillColor = NULL;
		[self setFill:FILL_NONE];
//	    } else if (!gFlags.eofill && !gFlags.fill) {	// why did I add this code here?
//		gFlags.fill = YES;
	    }
	}
    }

    if (version >= ROTATED_VERSION)
    {
    	NXReadTypes(stream, "f", &rotAngle);
	NXReadTypes(stream, "ff", &unrotOrigin.x, &unrotOrigin.y );
    }

    return self;
}

/* Routines which may need subclassing for different Graphic types. */

- (BOOL)constrainByDefault
{
    return NO;
}

- constrainCorner:(int)corner toAspectRatio:(float)aspect
/*
 * Modifies the bounds rectangle by moving the specified corner so that
 * the Graphic maintains the specified aspect ratio.  This is used during
 * constrained resizing.  Can be overridden if the aspect ratio is not
 * sufficient to constrain resizing.
 */
{
    int newcorner;
    float actualAspect;

    if (!bounds.size.height || !bounds.size.width || !aspect) return self;
    actualAspect = bounds.size.width / bounds.size.height;
    if (actualAspect == aspect) return self;

    switch (corner) {
    case LEFT_SIDE:
	bounds.origin.x -= bounds.size.height * aspect-bounds.size.width;
    case RIGHT_SIDE:
	bounds.size.width = bounds.size.height * aspect;
	if (bounds.size.width) NXIntegralRect(&bounds);
	return self;
    case BOTTOM_SIDE:
	bounds.origin.y -= bounds.size.width / aspect-bounds.size.height;
    case TOP_SIDE:
	bounds.size.height = bounds.size.width / aspect;
	if (bounds.size.height) NXIntegralRect(&bounds);
	return self;
    case LOWER_LEFT:
	corner = 0;
    case 0:
    case UPPER_RIGHT:
    case UPPER_LEFT:
    case LOWER_RIGHT:
	if (actualAspect > aspect) {
	    newcorner = ((corner|KNOB_DY_ONCE)&(~(KNOB_DY_TWICE)));
	} else {
	    newcorner = ((corner|KNOB_DX_ONCE)&(~(KNOB_DX_TWICE)));
	}
	return [self constrainCorner:newcorner toAspectRatio:aspect];
    default:
	return self;
    }
}

#define RESIZE_MASK (NX_MOUSEUPMASK|NX_MOUSEDRAGGEDMASK|NX_TIMERMASK)

- resize:(NXEvent *)event by:(int)corner in:(GraphicView *)view
/*
 * Resizes the graphic by the specified corner.  If corner == CREATE,
 * then it is resized by the UPPER_RIGHT corner, but the initial size
 * is reset to 1 by 1.
 */
{
    NXPoint p, last;
    float aspect = 0.0;
    Window *window = [view window];
    BOOL constrain, canScroll;
    DrawStatusType oldDrawStatus;
    NXTrackingTimer *timer = NULL;
    NXRect eb, starteb, oldeb, visibleRect;

    if (!gFlags.active || !gFlags.selected || !corner) return self;

    constrain = ((event->flags & NX_ALTERNATEMASK) &&
	((bounds.size.width && bounds.size.height) || corner == CREATE));
    if ([self constrainByDefault]) constrain = !constrain;
    if (constrain) aspect = bounds.size.width / bounds.size.height;
    if (corner == CREATE) {
	bounds.size.width = bounds.size.height = 1.0;
	corner = UPPER_RIGHT;
    }

    gFlags.selected = NO;

    [self getExtendedBounds:&eb];
    [view lockFocus];
    gFlags.active = NO;
    [view cache:&eb andUpdateLinks:NO];
    gFlags.active = YES;
    starteb = eb;
    [self draw:NULL];
    [window flushWindow];

    oldDrawStatus = DrawStatus;
    DrawStatus = Resizing;

    [view getVisibleRect:&visibleRect];
    canScroll = !NXEqualRect(&visibleRect, &bounds);
    if (canScroll && !timer) timer = NXBeginTimer(NULL, 0.1, 0.1);

    while (event->type != NX_MOUSEUP) {
	p = event->location;
	event = [NXApp getNextEvent:RESIZE_MASK];
	if (event->type == NX_TIMER) event->location = p;
	p = event->location;
	[view convertPoint:&p fromView:nil];
	[view grid:&p];
	if (p.x != last.x || p.y != last.y) {
	    corner = [self moveCorner:corner to:&p constrain:constrain];
	    if (constrain) [self constrainCorner:corner toAspectRatio:aspect];
	    oldeb = eb;
	    [self getExtendedBounds:&eb];
	    [window disableFlushWindow];
	    [view drawSelf:&oldeb :1];
	    if (canScroll) {
		[view scrollPointToVisible:&p]; // actually we want to keep the "edges" of the
						// Graphic being resized that were visible when
						// the resize started visible throughout the
						// resizing time (this will be difficult if those
						// edges flip from being the left edge to the
						// right edge in the middle of the resize!).
	    }
	    [self draw:NULL];
	    [view tryToPerform:@selector(updateRulers:) with:(void *)&bounds];
	    [window reenableFlushWindow];
	    [window flushWindow];
	    last = p;
	    NXPing();
	}
    }

    if (canScroll && timer) {
	NXEndTimer(timer);
	timer = NULL;
    }

    gFlags.selected = YES;
    DrawStatus = oldDrawStatus;

    [view cache:&eb andUpdateLinks:NO];	// redraw after resizing a Graphic
    NXUnionRect(&eb, &starteb);
    [view updateTrackedLinks:&starteb];
    [view tryToPerform:@selector(updateRulers:) with:nil];
    [window flushWindow];
    [view unlockFocus];

    return self;
}

- (BOOL)create:(NXEvent *)event in:(GraphicView *)view
/*
 * This method rarely needs to be subclassed.
 * It sets up an initial bounds, and calls resize:by:in:.
 */
{
    BOOL valid;
    NXCoord gridSpacing;

    bounds.origin = event->location;
    [view convertPoint:&bounds.origin fromView:nil];
    [view grid:&bounds.origin];

    gridSpacing = (NXCoord)[view gridSpacing];
    bounds.size.height = gridSpacing;
    bounds.size.width = gridSpacing * [self naturalAspectRatio];

    [self resize:event by:CREATE in:view];

    valid = [self isValid];

    if (valid) {
	gFlags.selected = YES;
	gFlags.active = YES;
    } else {
	gFlags.selected = NO;
	gFlags.active = NO;
	[view display];
    }

    return valid;
}

- (BOOL)hit:(const NXPoint *)p
{
    return (!gFlags.locked && gFlags.active && NXMouseInRect(p, &bounds, NO));
}

- (BOOL)isOpaque
{
    return [self fill] ? YES : NO;
}

- (BOOL)isValid
/*
 * Called after a Graphic is created to see if it is valid (this usually
 * means "is it big enough?").
 */
{
    return (bounds.size.width > MINSIZE && bounds.size.height > MINSIZE);
}

- (float)naturalAspectRatio
/*
 * A natural aspect ratio of zero means it doesn't have a natural aspect ratio.
 */
{
    return 0.0;
}

- (int)moveCorner:(int)corner to:(const NXPoint *)p constrain:(BOOL)flag
/*
 * Moves the specified corner to the specified point.
 * Returns the position of the corner after it was moved.
 */
{
    int newcorner = corner;

    if ((corner & KNOB_DX_ONCE) && (corner & KNOB_DX_TWICE)) {
	bounds.size.width += p->x - (bounds.origin.x + bounds.size.width);
	if (bounds.size.width <= 0.0) {
	    newcorner &= ~ (KNOB_DX_ONCE | KNOB_DX_TWICE);
	    bounds.origin.x += bounds.size.width;
	    bounds.size.width = - bounds.size.width;
	}
    } else if (!(corner & KNOB_DX_ONCE)) {
	bounds.size.width += bounds.origin.x - p->x;
	bounds.origin.x = p->x;
	if (bounds.size.width <= 0.0) {
	    newcorner |= KNOB_DX_ONCE | KNOB_DX_TWICE;
	    bounds.origin.x += bounds.size.width;
	    bounds.size.width = - bounds.size.width;
	}
    }

    if ((corner & KNOB_DY_ONCE) && (corner & KNOB_DY_TWICE)) {
	bounds.size.height += p->y - (bounds.origin.y + bounds.size.height);
	if (bounds.size.height <= 0.0) {
	    newcorner &= ~ (KNOB_DY_ONCE | KNOB_DY_TWICE);
	    bounds.origin.y += bounds.size.height;
	    bounds.size.height = - bounds.size.height;
	}
    } else if (!(corner & KNOB_DY_ONCE)) {
	bounds.size.height += bounds.origin.y - p->y;
	bounds.origin.y = p->y;
	if (bounds.size.height <= 0.0) {
	    newcorner |= KNOB_DY_ONCE | KNOB_DY_TWICE;
	    bounds.origin.y += bounds.size.height;
	    bounds.size.height = - bounds.size.height;
	}
    }

    if (newcorner != LOWER_LEFT) newcorner &= 0xf;
    if (!newcorner) newcorner = LOWER_LEFT;

    return newcorner;
}

- unitDraw
/*
 * If a Graphic just wants to draw itself in the bounding box of
 * {{0.0,0.0},{1.0,1.0}}, it can simply override this method.
 * Everything else will work fine.
 */
{
    return self;
}

- draw
/*
 * Almost all Graphics need to override this method.
 * It does the Graphic-specific drawing.
 * By default, it scales the coordinate system and calls unitDraw.
 */
{
    if (bounds.size.width >= 1.0 && bounds.size.height >= 1.0) {
	PStranslate(bounds.origin.x, bounds.origin.y);
	PSscale(bounds.size.width, bounds.size.height);
	[self unitDraw];
    }
    return self;
}

- (BOOL)edit:(NXEvent *)event in:(View *)view
/*
 * Any Graphic which has editable text should override this method
 * to edit that text.  TextGraphic is an example.
 */
{
    return NO;
}

/* Methods added to support Hippo drawing */

- forward:(SEL)aSelector :(marg_list)argFrame
{
    return self;
}
@end

These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.