Browse Source

Plan 9 from Bell Labs 2004-02-10

David du Colombier 20 years ago
parent
commit
3ce71f45f3
4 changed files with 2179 additions and 7 deletions
  1. 10 7
      dist/replica/plan9.db
  2. 10 0
      dist/replica/plan9.log
  3. 152 0
      sys/man/1/gview
  4. 2007 0
      sys/src/cmd/gview.c

+ 10 - 7
dist/replica/plan9.db

@@ -27,7 +27,7 @@
 386/bin/9660srv - 775 sys sys 1064598019 104963
 386/bin/aan - 775 sys sys 1064598020 128816
 386/bin/acid - 775 sys sys 1073851203 379815
-386/bin/acme - 775 sys sys 1073851204 422376
+386/bin/acme - 775 sys sys 1076385619 422401
 386/bin/ape - 20000000775 sys sys 1016944144 0
 386/bin/ape/basename - 775 sys sys 1071245280 133877
 386/bin/ape/cc - 775 sys sys 1064598025 68790
@@ -251,6 +251,7 @@
 386/bin/grep - 775 sys sys 1064598154 77644
 386/bin/gs - 775 sys sys 1071245334 11134067
 386/bin/gunzip - 775 sys sys 1064598211 79675
+386/bin/gview - 775 sys sys 1076385620 235602
 386/bin/gzip - 775 sys sys 1064598211 83770
 386/bin/hayes - 775 sys sys 1064598212 63307
 386/bin/hget - 775 sys sys 1071245335 222506
@@ -369,7 +370,7 @@
 386/bin/replica/setupdirs - 775 sys sys 1020319083 44
 386/bin/replica/updatedb - 775 sys sys 1068558250 95126
 386/bin/resample - 775 sys sys 1064598294 120124
-386/bin/rio - 775 sys sys 1073851237 311078
+386/bin/rio - 775 sys sys 1076385621 311170
 386/bin/rm - 775 sys sys 1064598298 59786
 386/bin/rtstats - 775 sys sys 1071245345 177818
 386/bin/rx - 775 sys sys 1071245345 80158
@@ -443,7 +444,7 @@
 386/bin/upas/msgcat - 775 sys sys 1064598353 38
 386/bin/upas/msgtok - 775 sys sys 1064598354 75149
 386/bin/upas/nedmail - 775 sys sys 1075097690 154056
-386/bin/upas/pop3 - 775 sys sys 1073851246 259903
+386/bin/upas/pop3 - 775 sys sys 1076385622 259917
 386/bin/upas/qer - 775 sys sys 1073851247 98840
 386/bin/upas/ratfs - 775 sys sys 1071245366 112106
 386/bin/upas/runq - 775 sys sys 1073851247 111600
@@ -461,8 +462,8 @@
 386/bin/usb/usbaudio - 775 sys sys 1073851250 175810
 386/bin/usb/usbd - 775 sys sys 1064598370 122722
 386/bin/usb/usbmouse - 775 sys sys 1064598371 137915
-386/bin/vac - 775 sys sys 1073851251 171848
-386/bin/vacfs - 775 sys sys 1073851251 178021
+386/bin/vac - 775 sys sys 1076385622 171944
+386/bin/vacfs - 775 sys sys 1076385622 178117
 386/bin/venti - 20000000775 sys sys 947360466 0
 386/bin/venti/buildindex - 775 sys sys 1073851252 161262
 386/bin/venti/checkarenas - 775 sys sys 1073851252 165195
@@ -485,7 +486,7 @@
 386/bin/vncs - 775 sys sys 1064598394 442530
 386/bin/vncv - 775 sys sys 1071245371 271560
 386/bin/vt - 775 sys sys 1071245372 171391
-386/bin/vtdump - 775 sys sys 1073851258 161461
+386/bin/vtdump - 775 sys sys 1076385623 161557
 386/bin/wc - 775 sys sys 1064598399 42148
 386/bin/webcookies - 775 sys sys 1068558171 162995
 386/bin/webfs - 775 sys sys 1068558224 352388
@@ -527,7 +528,7 @@
 386/lib/libbin.a - 664 sys sys 1073851265 2556
 386/lib/libbio.a - 664 sys sys 1073851265 28338
 386/lib/libc.a - 664 sys sys 1073851266 504454
-386/lib/libcomplete.a - 664 sys sys 1073851266 6152
+386/lib/libcomplete.a - 664 sys sys 1076385623 6210
 386/lib/libcontrol.a - 664 sys sys 1073851267 242856
 386/lib/libdisk.a - 664 sys sys 1073851267 43536
 386/lib/libdraw.a - 664 sys sys 1073851268 372528
@@ -4642,6 +4643,7 @@ sys/man/1/grap - 664 sys sys 944959675 6417
 sys/man/1/graph - 664 sys sys 944959673 2858
 sys/man/1/grep - 664 sys sys 969512677 2224
 sys/man/1/gs - 664 sys sys 1032054727 6906
+sys/man/1/gview - 664 sys sys 1076349870 4219
 sys/man/1/gzip - 664 sys sys 969499884 3073
 sys/man/1/hget - 664 sys sys 1067722859 1483
 sys/man/1/history - 664 sys sys 1044909169 1709
@@ -9362,6 +9364,7 @@ sys/src/cmd/gs/zlib/zlib.h - 664 sys sys 1015035350 36326
 sys/src/cmd/gs/zlib/zlib.rc - 644 sys sys 1015035349 879
 sys/src/cmd/gs/zlib/zutil.c - 664 sys sys 1015035350 5015
 sys/src/cmd/gs/zlib/zutil.h - 664 sys sys 1015035350 4945
+sys/src/cmd/gview.c - 664 sys sys 1076349870 50594
 sys/src/cmd/gzip - 20000000775 sys sys 984789976 0
 sys/src/cmd/gzip/gunzip.c - 664 sys sys 1014924864 6039
 sys/src/cmd/gzip/gzip.c - 664 sys sys 1014924864 3646

+ 10 - 0
dist/replica/plan9.log

@@ -13802,3 +13802,13 @@
 1076277660 1 c sys/man/2/complete - 664 sys sys 1076276513 2396
 1076277660 2 c sys/src/cmd/rio/wind.c - 664 sys sys 1076276532 32785
 1076277660 3 c sys/src/libcomplete/complete.c - 664 sys sys 1076276513 2697
+1076351472 0 a sys/man/1/gview - 664 sys sys 1076349870 4219
+1076351472 1 a sys/src/cmd/gview.c - 664 sys sys 1076349870 50594
+1076385677 0 c 386/bin/acme - 775 sys sys 1076385619 422401
+1076385677 1 c 386/bin/rio - 775 sys sys 1076385621 311170
+1076385677 2 c 386/bin/vtdump - 775 sys sys 1076385623 161557
+1076385677 3 a 386/bin/gview - 775 sys sys 1076385620 235602
+1076385677 4 c 386/bin/upas/pop3 - 775 sys sys 1076385622 259917
+1076385677 5 c 386/bin/vac - 775 sys sys 1076385622 171944
+1076385677 6 c 386/bin/vacfs - 775 sys sys 1076385622 178117
+1076385677 7 c 386/lib/libcomplete.a - 664 sys sys 1076385623 6210

+ 152 - 0
sys/man/1/gview

@@ -0,0 +1,152 @@
+.TH GVIEW 1
+.SH NAME
+gview \- interactive graph viewer
+.SH SYNOPSIS
+.B gview
+[
+.B -l
+.I logfile
+]
+[
+.B -m
+]
+[
+.I file
+]
+.SH DESCRIPTION
+.I Gview
+reads polygonal lines or a polygonal line drawing from an
+.B ASCII
+input file (which defaults to standard input), and views it interactively,
+with commands to zoom in and out, perform simple editing operations, and
+display information about points and polylines.  The editing commands can
+change the color and thickness of the polylines, delete (or undelete)
+some of them, and optionally rotate and move them.  It is also possible to
+generate an output file that reflects these changes and is in the same format
+as the input.
+.PP
+Since the
+.B move
+and
+.B rotate
+commands are undesirable when just viewing a graph, they are only enabled if
+.I gview
+is invoked with the
+.B -m
+option.
+.PP
+Clicking on a polyline with button 1 displays the coordinates and a
+.I t
+value that tells how far along the polyline.
+.IR (t =0
+at the first vertex,
+.IR t =1
+at the first vertex,
+.IR t =1.5
+halfway between the second and third vertices, etc.)  The
+.B -l
+option generates a log file that lists all points selected in this manner.
+.PP
+The most important interactive operations are to
+.I zoom in
+by sweeping out a rectangle, or to
+.I zoom out
+so that everything currently being displayed shrinks to fit in the swept-out
+rectangle.  Other options on the button 3 menu are
+.I unzoom
+which restores the coordinate system to the default state where everything
+fits on the screen,
+.I recenter
+which takes a point and makes it the center of the window, and
+.I square up
+which makes the horizontal and vertical scale factors equal.
+.PP
+To take a graph of a function where some part is almost linear and
+see how it deviates from a straight line, select two points on this
+part of the graph (i.e., select one with button 1 and then select the
+other) and then use the
+.I slant
+command on the button 3 menu.
+This slants the coordinate system so that the line between the two
+selected points appears horizontal (but vertical still means positive
+.IR y ).
+Then the
+.I zoom in
+command can be used to accentuate deviations from horizontal.
+There is also an
+.I unslant
+command that undoes all of this and goes back to an unslanted coordinate
+system.
+.PP
+There is a
+.I recolor
+command on button 3 that lets you select a color and change everything
+to have that color, and a similar command on button 2 that only affects
+the selected polyline.  The
+.I thick
+or
+.I thin
+command on button 2 changes the thickness of the selected polyline
+and there is also an undo command for such edits.
+.PP
+Finally, button 3 had commands to
+.I read
+a new input file and display it on top of everything else,
+.I restack
+the drawing order (in case lines of different color are drawn on top of
+each other),
+.I write
+everything into an output file, or
+.I exit
+the program.
+.PP
+Each polyline in an input or output file is a space-delimited
+.I x
+.I y
+coordinate pair on a line by itself, and the polyline is a sequence
+of such vertices followed by a label.  The label could be just a
+blank line or it could be a string in double
+quotes, or virtually any text that does not contain spaces and is
+on a line by itself.  The label at the end of the last polyline is
+optional.   It is not legal to have two consecutive labels, since that
+would denote a zero-vertex polyline and each polyline must have at least
+one vertex. (One-vertex polylines are useful for scatter plots.)
+
+If the label after a polyline can contains the word
+.B "Thick"
+or a color name
+.BR (Red ,
+.BR Pink ,
+.BR Dkred ,
+.BR Orange ,
+.BR Yellow ,
+.BR Dkyellow ,
+.BR Green ,
+.BR Dkgreen ,
+.BR Cyan ,
+.BR Blue ,
+.BR Ltblue ,
+.BR Magenta ,
+.BR Violet ,
+.BR Gray ,
+.BR Black ,
+.BR White ),
+whichever color name comes first will be used to color the polyline.
+.SH EXAMPLE
+To see a graph of the function
+.IR y = sin( x )/ x ,
+generate input with an awk script and pipe it into
+.IR gview :
+.IP
+.EX
+awk 'BEGIN{for(x=.1;x<500;x+=.1)print x,sin(x)/x}' | gview
+.EE
+.SH SOURCE
+.B /sys/src/cmd/gview.c
+.SH SEE ALSO
+.IR awk (1)
+.SH BUGS
+The user interface for the
+.I slant
+command is counter-intuitive.  Perhaps it would be better to have a scheme
+for sweeping out a parallelogram.

+ 2007 - 0
sys/src/cmd/gview.c

@@ -0,0 +1,2007 @@
+#include	<u.h>
+#include	<libc.h>
+#include	<ctype.h>
+#include	<draw.h>
+#include	<event.h>
+#include	<cursor.h>
+#include	<stdio.h>
+
+#define Never	0xffffffff	/* Maximum ulong */
+#define LOG2  0.301029995664
+#define Button_bit(b)	(1 << ((b)-1))
+
+enum {
+	But1	= Button_bit(1),/* mouse buttons for events */
+	But2	= Button_bit(2),
+	But3	= Button_bit(3),
+};
+int cantmv = 1;			/* disallow rotate and move? 0..1 */
+int top_border, bot_border, lft_border, rt_border;
+int lft_border0;		/* lft_border for y-axis labels >0 */
+int top_left, top_right;	/* edges of top line free space */
+int Mv_delay = 400;		/* msec for button click vs. button hold down */
+int Dotrad = 2;			/* dot radius in pixels */
+int framewd=1;			/* line thickness for frame (pixels) */
+int framesep=1;			/* distance between frame and surrounding text */
+int outersep=1;			/* distance: surrounding text to screen edge */
+Point sdigit;			/* size of a digit in the font */
+Point smaxch;			/* assume any character in font fits in this */
+double underscan = .05;		/* fraction of frame initially unused per side */
+double fuzz = 6;		/* selection tolerance in pixels */
+int tick_len = 15;		/* length of axis label tick mark in pixels */
+FILE* logfil = 0;		/* dump selected points here if nonzero */
+
+#define labdigs  3		/* allow this many sig digits in axis labels */
+#define digs10pow 1000		/* pow(10,labdigs) */
+#define axis_color  clr_im(DLtblue)
+
+
+
+
+/********************************* Utilities  *********************************/
+
+/* Prepend string s to null-terminated string in n-byte buffer buf[], truncating if
+   necessary and using a space to separate s from the rest of buf[].
+*/
+char* str_insert(char* buf, char* s, int n)
+{
+	int blen, slen = strlen(s) + 1;
+	if (slen >= n)
+		{strncpy(buf,s,n); buf[n-1]='\0'; return buf;}
+	blen = strlen(buf);
+	if (blen >= n-slen)
+		buf[blen=n-slen-1] = '\0';
+	memmove(buf+slen, buf, slen+blen+1);
+	memcpy(buf, s, slen-1);
+	buf[slen-1] = ' ';
+	return buf;
+}
+
+/* Alter string smain (without lengthening it) so as to remove the first occurrence of
+   ssub, assuming ssub is ASCII.  Return nonzero (true) if string smain had to be changed.
+   In spite of the ASCII-centric appearance, I think this can handle UTF in smain.
+*/
+int remove_substr(char* smain, char* ssub)
+{
+	char *ss, *s = strstr(smain, ssub);
+	int n = strlen(ssub);
+	if (s==0)
+		return 0;
+	if (islower(s[n]))
+		s[0] ^= 32;			/* probably tolower(s[0]) or toupper(s[0]) */
+	else {
+		for (ss=s+n; *ss!=0; s++, ss++)
+			*s = *ss;
+		*s = '\0';
+	}
+	return 1;
+}
+
+void adjust_border(Font* f)
+{
+	int sep = framesep + outersep;
+	sdigit = stringsize(f, "8");
+	smaxch = stringsize(f, "MMMg");
+	smaxch.x = (smaxch.x + 3)/4;
+	lft_border0 = (1+labdigs)*sdigit.x + framewd + sep;
+	rt_border = (lft_border0 - sep)/2 + outersep;
+	bot_border = sdigit.y + framewd + sep;
+	top_border = smaxch.y + framewd + sep;
+	lft_border = lft_border0;		/* this gets reset later */
+}
+
+
+int is_off_screen(Point p)
+{
+	const Rectangle* r = &(screen->r);
+	return p.x-r->min.x<lft_border || r->max.x-p.x<rt_border
+		|| p.y-r->min.y<=top_border || r->max.y-p.y<=bot_border;
+}
+
+
+Cursor	bullseye =
+{
+	{-7, -7},
+	{
+		0x1F, 0xF8, 0x3F, 0xFC, 0x7F, 0xFE, 0xFB, 0xDF,
+	 	0xF3, 0xCF, 0xE3, 0xC7, 0xFF, 0xFF, 0xFF, 0xFF,
+	 	0xFF, 0xFF, 0xFF, 0xFF, 0xE3, 0xC7, 0xF3, 0xCF,
+	 	0x7B, 0xDF, 0x7F, 0xFE, 0x3F, 0xFC, 0x1F, 0xF8,
+	},
+	{
+		0x00, 0x00, 0x0F, 0xF0, 0x31, 0x8C, 0x21, 0x84,
+		0x41, 0x82, 0x41, 0x82, 0x41, 0x82, 0x7F, 0xFE,
+		0x7F, 0xFE, 0x41, 0x82, 0x41, 0x82, 0x41, 0x82,
+		0x21, 0x84, 0x31, 0x8C, 0x0F, 0xF0, 0x00, 0x00,
+	}
+};
+
+int get_1click(int but, Mouse* m, Cursor* curs)
+{
+	if (curs)
+		esetcursor(curs);
+	while (m->buttons==0)
+		*m = emouse();
+	if (curs)
+		esetcursor(0);
+	return (m->buttons==Button_bit(but));
+}
+
+
+/* Wait until but goes up or until a mouse event's msec passes tlimit.
+   Return a boolean result that tells whether the button went up.
+*/
+int lift_button(int but, Mouse* m, int tlimit)
+{
+	do {	*m = emouse();
+		if (m->msec >= tlimit)
+			return 0;
+	} while (m->buttons & Button_bit(but));
+	return 1;
+}
+
+
+/* Set *m to the last pending mouse event, or the first one where but is up.
+   If no mouse events are pending, wait for the next one.
+*/
+void latest_mouse(int but, Mouse* m)
+{
+	int bbit = Button_bit(but);
+	do {	*m = emouse();
+	} while ((m->buttons & bbit) && ecanmouse());
+}
+
+
+
+/*********************************** Colors ***********************************/
+
+enum {	DOrange=0xffaa00FF, Dgray=0xbbbbbbFF, DDkgreen=0x009900FF,
+	DDkred=0xcc0000FF, DViolet=0x990099FF, DDkyellow=0xaaaa00FF,
+	DLtblue=0xaaaaffFF, DPink=0xffaaaaFF,
+	/* ndraw.h sets DBlack, DBlue, DRed, DYellow, DGreen,
+		DCyan, DMagenta, DWhite */
+};
+
+typedef struct color_ref {
+	ulong c;			/* RGBA pixel color */
+	char* nam;			/* ASCII name (matched to input, used in output)*/
+	Image* im;			/* replicated solid-color image */
+} color_ref;
+
+color_ref clrtab[] = {
+	DRed,		"Red",		0,
+	DPink,		"Pink",		0,
+	DDkred,		"Dkred",	0,
+	DOrange,	"Orange",	0,
+	DYellow,	"Yellow",	0,
+	DDkyellow,	"Dkyellow",	0,
+	DGreen,		"Green",	0,
+	DDkgreen,	"Dkgreen",	0,
+	DCyan,		"Cyan",		0,
+	DBlue,		"Blue",		0,
+	DLtblue,	"Ltblue",	0,
+	DMagenta,	"Magenta",	0,
+	DViolet,	"Violet",	0,
+	Dgray,		"Gray",		0,
+	DBlack,		"Black",	0,
+	DWhite,		"White",	0,
+	DNofill,	0,		0	/* DNofill means "end of data" */
+};
+
+
+void  init_clrtab(void)
+{
+	int i;
+	Rectangle r = Rect(0,0,1,1);
+	for (i=0; clrtab[i].c!=DNofill; i++)
+		clrtab[i].im = allocimage(display, r, CMAP8, 1, clrtab[i].c);
+		/* should check for 0 result? */
+}
+
+
+int clrim_id(Image* clr)
+{
+	int i;
+	for (i=0; clrtab[i].im!=clr; i++)
+		if (clrtab[i].c==DNofill)
+			exits("bad image color");
+	return i;
+}
+
+int clr_id(int clr)
+{
+	int i;
+	for (i=0; clrtab[i].c!=clr; i++)
+		if (clrtab[i].c==DNofill)
+			exits("bad color");
+	return i;
+}
+
+#define clr_im(clr)	clrtab[clr_id(clr)].im
+
+
+/* This decides what color to use for a polyline based on the label it has in the
+   input file.  Whichever color name comes first is the winner, otherwise return black.
+*/
+Image* nam2clr(const char* nam, int *idxdest)
+{
+	char *c, *cbest=nam;
+	int i, ibest=-1;
+	if (*nam!=0)
+		for (i=0; clrtab[i].nam!=0; i++) {
+			c = strstr(nam,clrtab[i].nam);
+			if (c!=0 && (ibest<0 || c<cbest))
+				{ibest=i; cbest=c;}
+		}
+	if (idxdest!=0)
+		*idxdest = (ibest<0) ? clr_id(DBlack) : ibest;
+	return (ibest<0) ? clr_im(DBlack) : clrtab[ibest].im;
+}
+
+/* A polyline is initial drawn in thick mode iff its label in the file contains "Thick" */
+int nam2thick(const char* nam)
+{
+	return strstr(nam,"Thick")==0 ? 0 : 1;
+}
+
+
+/* Alter string nam so that nam2thick() and nam2clr() agree with th and clr, using
+   buf[] (a buffer of length bufn) to store the result if it differs from nam.
+   We go to great pains to perform this alteration in a manner that will seem natural
+   to the user, i.e., we try removing a suitably isolated color name before inserting
+   a new one.
+*/
+char* nam_with_thclr(char* nam, int th, Image* clr, char* buf, int bufn)
+{
+	int clr0i, th0=nam2thick(nam);
+	Image* clr0 = nam2clr(nam, &clr0i);
+	char *clr0s;
+	if (th0==th && clr0==clr)
+		return nam;
+	clr0s = clrtab[clr0i].nam;
+	if (strlen(nam)<bufn) strcpy(buf,nam);
+	else {strncpy(buf,nam,bufn); buf[bufn-1]='\0';}
+	if (clr0 != clr)
+		remove_substr(buf, clr0s);
+	if (th0 > th)
+		while (remove_substr(buf, "Thick"))
+			/* do nothing */;
+	if (nam2clr(buf,0) != clr)
+		str_insert(buf, clrtab[clrim_id(clr)].nam, bufn);
+	if (th0 < th)
+		str_insert(buf, "Thick", bufn);
+	return buf;
+}
+
+
+
+/****************************** Data structures  ******************************/
+
+Image* mv_bkgd;				/* Background image (usually 0) */
+
+typedef struct fpoint {
+	double x, y;
+} fpoint;
+
+typedef struct frectangle {
+	fpoint min, max;
+} frectangle;
+
+frectangle empty_frect = {1e30, 1e30, -1e30, -1e30};
+
+
+/* When *r2 is transformed by y=y-x*slant, might it intersect *r1 ?
+*/
+int fintersects(const frectangle* r1, const frectangle* r2, double slant)
+{
+	double x2min=r2->min.x, x2max=r2->max.x;
+	if (r1->max.x <= x2min || x2max <= r1->min.x)
+		return 0;
+	if (slant >=0)
+		{x2min*=slant; x2max*=slant;}
+	else	{double t=x2min*slant; x2min=x2max*slant; x2max=t;}
+	return r1->max.y > r2->min.y-x2max && r2->max.y-x2min > r1->min.y;
+}
+
+int fcontains(const frectangle* r, fpoint p)
+{
+	return r->min.x <=p.x && p.x<= r->max.x && r->min.y <=p.y && p.y<= r->max.y;
+}
+
+
+void grow_bb(frectangle* dest, const frectangle* r)
+{
+	if (r->min.x < dest->min.x) dest->min.x=r->min.x;
+	if (r->min.y < dest->min.y) dest->min.y=r->min.y;
+	if (r->max.x > dest->max.x) dest->max.x=r->max.x;
+	if (r->max.y > dest->max.y) dest->max.y=r->max.y;
+}
+
+
+void slant_frect(frectangle *r, double sl)
+{
+	r->min.y += sl*r->min.x;
+	r->max.y += sl*r->max.x;
+}
+
+
+fpoint fcenter(const frectangle* r)
+{
+	fpoint c;
+	c.x = .5*(r->max.x + r->min.x);
+	c.y = .5*(r->max.y + r->min.y);
+	return c;
+}
+
+
+typedef struct fpolygon {
+	fpoint* p;			/* a malloc'ed array */
+	int n;				/* p[] has n elements: p[0..n] */
+	frectangle bb;			/* bounding box */
+	char* nam;			/* name of this polygon (malloc'ed) */
+	int thick;			/* use 1+2*thick pixel wide lines */
+	Image* clr;			/* Color to use when drawing this */
+	struct fpolygon* link;
+} fpolygon;
+
+typedef struct fpolygons {
+	fpolygon* p;			/* the head of a linked list */
+	frectangle bb;			/* overall bounding box */
+	frectangle disp;		/* part being mapped onto screen->r */
+	double slant_ht;		/* controls how disp is slanted */
+} fpolygons;
+
+
+fpolygons univ = {			/* everything there is to display */
+	0,
+	1e30, 1e30, -1e30, -1e30,
+	0, 0, 0, 0,
+	2*1e30
+};
+
+
+void set_default_clrs(fpolygons* fps, fpolygon* fpstop)
+{
+	fpolygon* fp;
+	for (fp=fps->p; fp!=0 && fp!=fpstop; fp=fp->link) {
+		fp->clr = nam2clr(fp->nam,0);
+		fp->thick = nam2thick(fp->nam);
+	}
+}
+
+
+void fps_invert(fpolygons* fps)
+{
+	fpolygon *p, *r=0;
+	for (p=fps->p; p!=0;) {
+		fpolygon* q = p;
+		p = p->link;
+		q->link = r;
+		r = q;
+	}
+	fps->p = r;
+}
+
+
+void fp_remove(fpolygons* fps, fpolygon* fp)
+{
+	fpolygon *q, **p = &fps->p;
+	while (*p!=fp)
+		if (*p==0)
+			return;
+		else	p = &(*p)->link;
+	*p = fp->link;
+	fps->bb = empty_frect;
+	for (q=fps->p; q!=0; q=q->link)
+		grow_bb(&fps->bb, &q->bb);
+}
+
+
+/* The transform maps abstract fpoint coordinates (the ones used in the input)
+   to the current screen coordinates.  The do_untransform() macros reverses this.
+   If univ.slant_ht is not the height of univ.disp, the actual region in the
+   abstract coordinates is a parallelogram inscribed in univ.disp with two
+   vertical edges and two slanted slanted edges: slant_ht>0 means that the
+   vertical edges have height slant_ht and the parallelogram touches the lower
+   left and upper right corners of univ.disp; slant_ht<0 refers to a parallelogram
+   of height -slant_ht that touches the other two corners of univ.disp.
+   NOTE: the ytransform macro assumes that tr->sl times the x coordinate has
+   already been subtracted from yy.
+*/
+typedef struct transform {
+	double sl;
+	fpoint o, sc;		/* (x,y):->(o.x+sc.x*x, o.y+sc.y*y+sl*x) */
+} transform;
+
+#define do_transform(d,tr,s)	((d)->x = (tr)->o.x + (tr)->sc.x*(s)->x,  \
+				(d)->y = (tr)->o.y + (tr)->sc.y*(s)->y    \
+					+ (tr)->sl*(s)->x)
+#define do_untransform(d,tr,s)	((d)->x = (.5+(s)->x-(tr)->o.x)/(tr)->sc.x,    \
+				(d)->y = (.5+(s)->y-(tr)->sl*(d)->x-(tr)->o.y) \
+					/(tr)->sc.y)
+#define xtransform(tr,xx)	((tr)->o.x + (tr)->sc.x*(xx))
+#define ytransform(tr,yy)	((tr)->o.y + (tr)->sc.y*(yy))
+#define dxuntransform(tr,xx)	((xx)/(tr)->sc.x)
+#define dyuntransform(tr,yy)	((yy)/(tr)->sc.y)
+
+
+transform cur_trans(void)
+{
+	transform t;
+	Rectangle d = screen->r;
+	const frectangle* s = &univ.disp;
+	double sh = univ.slant_ht;
+	d.min.x += lft_border;
+	d.min.y += top_border;
+	d.max.x -= rt_border;
+	d.max.y -= bot_border;
+	t.sc.x = (d.max.x - d.min.x)/(s->max.x - s->min.x);
+	t.sc.y = -(d.max.y - d.min.y)/fabs(sh);
+	if (sh > 0) {
+		t.sl = -t.sc.y*(s->max.y-s->min.y-sh)/(s->max.x - s->min.x);
+		t.o.y = d.min.y - t.sc.y*s->max.y - t.sl*s->max.x;
+	} else {
+		t.sl = t.sc.y*(s->max.y-s->min.y+sh)/(s->max.x - s->min.x);
+		t.o.y = d.min.y - t.sc.y*s->max.y - t.sl*s->min.x;
+	}
+	t.o.x = d.min.x - t.sc.x*s->min.x;
+	return t;
+}
+
+
+double u_slant_amt(fpolygons *u)
+{
+	double sh=u->slant_ht, dy=u->disp.max.y - u->disp.min.y;
+	double dx = u->disp.max.x - u->disp.min.x;
+	return (sh>0) ? (dy-sh)/dx : -(dy+sh)/dx;
+}
+
+
+/* Set *y0 and *y1 to the lower and upper bounds of the set of y-sl*x values that
+   *u says to display, where sl is the amount of slant.
+*/
+double set_unslanted_y(fpolygons *u, double *y0, double *y1)
+{
+	double yy1, sl=u_slant_amt(u);
+	if (u->slant_ht > 0) {
+		*y0 = u->disp.min.y - sl*u->disp.min.x;
+		yy1 = *y0 + u->slant_ht;
+	} else {
+		yy1 = u->disp.max.y - sl*u->disp.min.x;
+		*y0 = yy1 + u->slant_ht;
+	}
+	if (y1 != 0)
+		*y1 = yy1;
+	return sl;
+}
+
+
+
+
+/*************************** The region to display ****************************/
+
+void nontrivial_interval(double *lo, double *hi)
+{
+	if (*lo >= *hi) {
+		double mid = .5*(*lo + *hi);
+		double tweak = 1e-6 + 1e-6*fabs(mid);
+		*lo = mid - tweak;
+		*hi = mid + tweak;
+	}
+}
+
+
+void init_disp(void)
+{
+	double dw = (univ.bb.max.x - univ.bb.min.x)*underscan;
+	double dh = (univ.bb.max.y - univ.bb.min.y)*underscan;
+	univ.disp.min.x = univ.bb.min.x - dw;
+	univ.disp.min.y = univ.bb.min.y - dh;
+	univ.disp.max.x = univ.bb.max.x + dw;
+	univ.disp.max.y = univ.bb.max.y + dh;
+	nontrivial_interval(&univ.disp.min.x, &univ.disp.max.x);
+	nontrivial_interval(&univ.disp.min.y, &univ.disp.max.y);
+	univ.slant_ht = univ.disp.max.y - univ.disp.min.y;	/* means no slant */
+}
+
+
+void recenter_disp(Point c)
+{
+	transform tr = cur_trans();
+	fpoint cc, off;
+	do_untransform(&cc, &tr, &c);
+	off.x = cc.x - .5*(univ.disp.min.x + univ.disp.max.x);
+	off.y = cc.y - .5*(univ.disp.min.y + univ.disp.max.y);
+	univ.disp.min.x += off.x;
+	univ.disp.min.y += off.y;
+	univ.disp.max.x += off.x;
+	univ.disp.max.y += off.y;
+}
+
+
+/* Find the upper-left and lower-right corners of the bounding box of the
+   parallelogram formed by untransforming the rectangle rminx, rminy, ... (given
+   in screen coordinates), and return the height of the parallelogram (negated
+   if it slopes downward).
+*/
+double untransform_corners(double rminx, double rminy, double rmaxx, double rmaxy,
+		fpoint *ul, fpoint *lr)
+{
+	fpoint r_ur, r_ul, r_ll, r_lr;	/* corners of the given recangle */
+	fpoint ur, ll;			/* untransformed versions of r_ur, r_ll */
+	transform tr = cur_trans();
+	double ht;
+	r_ur.x=rmaxx;  r_ur.y=rminy;
+	r_ul.x=rminx;  r_ul.y=rminy;
+	r_ll.x=rminx;  r_ll.y=rmaxy;
+	r_lr.x=rmaxx;  r_lr.y=rmaxy;
+	do_untransform(ul, &tr, &r_ul);
+	do_untransform(lr, &tr, &r_lr);
+	do_untransform(&ur, &tr, &r_ur);
+	do_untransform(&ll, &tr, &r_ll);
+	ht = ur.y - lr->y;
+	if (ll.x < ul->x)
+		ul->x = ll.x;
+	if (ur.y > ul->y)
+		ul->y = ur.y;
+	else	ht = -ht;
+	if (ur.x > lr->x)
+		lr->x = ur.x;
+	if (ll.y < lr->y)
+		lr->y = ll.y;
+	return ht;
+}
+
+
+void disp_dozoom(double rminx, double rminy, double rmaxx, double rmaxy)
+{
+	fpoint ul, lr;
+	double sh = untransform_corners(rminx, rminy, rmaxx, rmaxy, &ul, &lr);
+	if (ul.x==lr.x || ul.y==lr.y)
+		return;
+	univ.slant_ht = sh;
+	univ.disp.min.x = ul.x;
+	univ.disp.max.y = ul.y;
+	univ.disp.max.x = lr.x;
+	univ.disp.min.y = lr.y;
+	nontrivial_interval(&univ.disp.min.x, &univ.disp.max.x);
+	nontrivial_interval(&univ.disp.min.y, &univ.disp.max.y);
+}
+
+
+void disp_zoomin(Rectangle r)
+{
+	disp_dozoom(r.min.x, r.min.y, r.max.x, r.max.y);
+}
+
+
+void disp_zoomout(Rectangle r)
+{
+	double qminx, qminy, qmaxx, qmaxy;
+	double scx, scy;
+	Rectangle s = screen->r;
+	if (r.min.x==r.max.x || r.min.y==r.max.y)
+		return;
+	s.min.x += lft_border;
+	s.min.y += top_border;
+	s.max.x -= rt_border;
+	s.max.y -= bot_border;
+	scx = (s.max.x - s.min.x)/(r.max.x - r.min.x);
+	scy = (s.max.y - s.min.y)/(r.max.y - r.min.y);
+	qminx = s.min.x + scx*(s.min.x - r.min.x);
+	qmaxx = s.max.x + scx*(s.max.x - r.max.x);
+	qminy = s.min.y + scy*(s.min.y - r.min.y);
+	qmaxy = s.max.y + scy*(s.max.y - r.max.y);
+	disp_dozoom(qminx, qminy, qmaxx, qmaxy);
+}
+
+
+void expand2(double* a, double* b, double f)
+{
+	double mid = .5*(*a + *b);
+	*a = mid + f*(*a - mid);
+	*b = mid + f*(*b - mid);
+}
+
+void disp_squareup(void)
+{
+	double dx = univ.disp.max.x - univ.disp.min.x;
+	double dy = univ.disp.max.y - univ.disp.min.y;
+	dx /= screen->r.max.x - lft_border - screen->r.min.x - rt_border;
+	dy /= screen->r.max.y - bot_border - screen->r.min.y - top_border;
+	if (dx > dy)
+		expand2(&univ.disp.min.y, &univ.disp.max.y, dx/dy);
+	else	expand2(&univ.disp.min.x, &univ.disp.max.x, dy/dx);
+	univ.slant_ht = univ.disp.max.y - univ.disp.min.y;
+}
+
+
+/* Slant so that p and q appear at the same height on the screen and the
+   screen contains the smallest possible superset of what its previous contents.
+*/
+void slant_disp(fpoint p, fpoint q)
+{
+	double yll, ylr, yul, yur;	/* corner y coords of displayed parallelogram */
+	double sh, dy;
+	if (p.x == q.x)
+		return;
+	sh = univ.slant_ht;
+	if (sh > 0) {
+		yll=yul=univ.disp.min.y;  yul+=sh;
+		ylr=yur=univ.disp.max.y;  ylr-=sh;
+	} else {
+		yll=yul=univ.disp.max.y;  yll+=sh;
+		ylr=yur=univ.disp.min.y;  yur-=sh;
+	}
+	dy = (univ.disp.max.x-univ.disp.min.x)*(q.y - p.y)/(q.x - p.x);
+	dy -= ylr - yll;
+	if (dy > 0)
+		{yll-=dy; yur+=dy;}
+	else	{yul-=dy; ylr+=dy;}
+	if (ylr > yll) {
+		univ.disp.min.y = yll;
+		univ.disp.max.y = yur;
+		univ.slant_ht = yur - ylr;
+	} else {
+		univ.disp.max.y = yul;
+		univ.disp.min.y = ylr;
+		univ.slant_ht = ylr - yur;
+	}
+}
+
+
+
+
+/******************************** Ascii input  ********************************/
+
+void set_fbb(fpolygon* fp)
+{
+	fpoint lo=fp->p[0], hi=fp->p[0];
+	const fpoint *q, *qtop;
+	for (qtop=(q=fp->p)+fp->n; ++q<=qtop;) {
+		if (q->x < lo.x) lo.x=q->x;
+		if (q->y < lo.y) lo.y=q->y;
+		if (q->x > hi.x) hi.x=q->x;
+		if (q->y > hi.y) hi.y=q->y;
+	}
+	fp->bb.min = lo;
+	fp->bb.max = hi;
+}
+
+char* mystrdup(char* s)
+{
+	char *r, *t = strrchr(s,'"');
+	if (t==0) {
+		t = s + strlen(s);
+		while (t>s && (t[-1]=='\n' || t[-1]=='\r'))
+			t--;
+	}
+	r = malloc(1+(t-s));
+	memcpy(r, s, t-s);
+	r[t-s] = 0;
+	return r;
+}
+
+int is_valid_label(char* lab)
+{
+	char* t;
+	if (lab[0]=='"')
+		return (t=strrchr(lab,'"'))!=0 && t!=lab && strspn(t+1," \t\r\n")==strlen(t+1);
+	return strcspn(lab," \t")==strlen(lab);
+}
+
+/* Read a polyline and update the number of lines read.  A zero result indicates bad
+   syntax if *lineno increases; otherwise it indicates end of file.
+*/
+fpolygon* rd_fpoly(FILE* fin, int *lineno)
+{
+	char buf[256], junk[2];
+	fpoint q;
+	fpolygon* fp;
+	int allocn;
+	if (!fgets(buf,256,fin))
+		return 0;
+	(*lineno)++;
+	if (sscanf(buf,"%lg%lg%1s",&q.x,&q.y,junk) != 2)
+		return 0;
+	fp = malloc(sizeof(fpolygon));
+	allocn = 16;
+	fp->p = malloc(allocn*sizeof(fpoint));
+	fp->p[0] = q;
+	fp->n = 0;
+	fp->nam = "";
+	fp->thick = 0;
+	fp->clr = clr_im(DBlack);
+	while (fgets(buf,256,fin)) {
+		(*lineno)++;
+		if (sscanf(buf,"%lg%lg%1s",&q.x,&q.y,junk) != 2) {
+			if (!is_valid_label(buf))
+				{free(fp->p); free(fp); return 0;}
+			fp->nam = (buf[0]=='"') ? buf+1 : buf;
+			break;
+		}
+		if (++(fp->n) == allocn)
+			fp->p = realloc(fp->p, (allocn<<=1)*sizeof(fpoint));
+		fp->p[fp->n] = q;
+	}
+	fp->nam = mystrdup(fp->nam);
+	set_fbb(fp);
+	fp->link = 0;
+	return fp;
+}
+
+
+/* Read input into *fps and return 0 or a line number where there's a syntax error */
+int rd_fpolys(FILE* fin, fpolygons* fps)
+{
+	fpolygon *fp, *fp0=fps->p;
+	int lineno=0, ok_upto=0;
+	while ((fp=rd_fpoly(fin,&lineno)) != 0) {
+		ok_upto = lineno;
+		fp->link = fps->p;
+		fps->p = fp;
+		grow_bb(&fps->bb, &fp->bb);
+	}
+	set_default_clrs(fps, fp0);
+	return (ok_upto==lineno) ? 0 : lineno;
+}
+
+
+/* Read input from file fnam and return an error line no., -1 for "can't open"
+   or 0 for success.
+*/
+int doinput(char* fnam)
+{
+	FILE* fin = strcmp(fnam,"-")==0 ? stdin : fopen(fnam, "r");
+	int errline_or0;
+	if (fin==0)
+		return -1;
+	errline_or0 = rd_fpolys(fin, &univ);
+	fclose(fin);
+	return errline_or0;
+}
+
+
+
+/******************************** Ascii output ********************************/
+
+fpolygon* fp_reverse(fpolygon* fp)
+{
+	fpolygon* r = 0;
+	while (fp!=0) {
+		fpolygon* q = fp->link;
+		fp->link = r;
+		r = fp;
+		fp = q;
+	}
+	return r;
+}
+
+void wr_fpoly(FILE* fout, const fpolygon* fp)
+{
+	char buf[256];
+	int i;
+	for (i=0; i<=fp->n; i++)
+		fprintf(fout,"%.12g\t%.12g\n", fp->p[i].x, fp->p[i].y);
+	fprintf(fout,"\"%s\"\n", nam_with_thclr(fp->nam, fp->thick, fp->clr, buf, 256));
+}
+
+void wr_fpolys(FILE* fout, fpolygons* fps)
+{
+	fpolygon* fp;
+	fps->p = fp_reverse(fps->p);
+	for (fp=fps->p; fp!=0; fp=fp->link)
+		wr_fpoly(fout, fp);
+	fps->p = fp_reverse(fps->p);
+}
+
+
+int dooutput(char* fnam)
+{
+	FILE* fout = fopen(fnam, "w");
+	if (fout==0)
+		return 0;
+	wr_fpolys(fout, &univ);
+	fclose(fout);
+	return 1;
+}
+
+
+
+
+/************************ Clipping to screen rectangle ************************/
+
+/* Find the t values, 0<=t<=1 for which x0+t*(x1-x0) is between xlo and xhi,
+   or return 0 to indicate no such t values exist.  If returning 1, set *t0 and
+   *t1 to delimit the t interval.
+*/
+int do_xory(double x0, double x1, double xlo, double xhi, double* t0, double* t1)
+{
+	*t1 = 1.0;
+	if (x0<xlo) {
+		if (x1<xlo) return 0;
+		*t0 = (xlo-x0)/(x1-x0);
+		if (x1>xhi) *t1 = (xhi-x0)/(x1-x0);
+	} else if (x0>xhi) {
+		if (x1>xhi) return 0;
+		*t0 = (xhi-x0)/(x1-x0);
+		if (x1<xlo) *t1 = (xlo-x0)/(x1-x0);
+	} else {
+		*t0 = 0.0;
+		if (x1>xhi) *t1 = (xhi-x0)/(x1-x0);
+		else if (x1<xlo) *t1 = (xlo-x0)/(x1-x0);
+		else *t1 = 1.0;
+	}
+	return 1;
+}
+
+
+/* After mapping y to y-slope*x, what initial fraction of the *p to *q edge is
+   outside of *r?  Note that the edge could start outside *r, pass through *r,
+   and wind up outside again.
+*/
+double frac_outside(const fpoint* p, const fpoint* q, const frectangle* r,
+		double slope)
+{
+	double t0, t1, tt0, tt1;
+	double px=p->x, qx=q->x;
+	if (!do_xory(px, qx, r->min.x, r->max.x, &t0, &t1))
+		return 1;
+	if (!do_xory(p->y-slope*px, q->y-slope*qx, r->min.y, r->max.y, &tt0, &tt1))
+		return 1;
+	if (tt0 > t0)
+		t0 = tt0;
+	if (t1<=t0 || tt1<=t0)
+		return 1;
+	return t0;
+}
+
+
+/* Think of p0..pn as piecewise-linear function F(t) for t=0..pn-p0, and find
+   the maximum tt such that F(0..tt) is all inside of r, assuming p0 is inside.
+   Coordinates are transformed by y=y-x*slope before testing against r.
+*/
+double in_length(const fpoint* p0, const fpoint* pn, frectangle r, double slope)
+{
+	const fpoint* p = p0;
+	double px, py;
+	do if (++p > pn)
+		return pn - p0;
+	while (r.min.x<=(px=p->x) && px<=r.max.x
+			&& r.min.y<=(py=p->y-slope*px) && py<=r.max.y);
+	return (p - p0) - frac_outside(p, p-1, &r, slope);
+}
+
+
+/* Think of p0..pn as piecewise-linear function F(t) for t=0..pn-p0, and find
+   the maximum tt such that F(0..tt) is all outside of *r.  Coordinates are
+   transformed by y=y-x*slope before testing against r.
+*/
+double out_length(const fpoint* p0, const fpoint* pn, frectangle r, double slope)
+{
+	const fpoint* p = p0;
+	double fr;
+	do {	if (p->x < r.min.x)
+			do if (++p>pn) return pn-p0;
+			while (p->x <= r.min.x);
+		else if (p->x > r.max.x)
+			do if (++p>pn) return pn-p0;
+			while (p->x >= r.max.x);
+		else if (p->y-slope*p->x < r.min.y)
+			do if (++p>pn) return pn-p0;
+			while (p->y-slope*p->x <= r.min.y);
+		else if (p->y-slope*p->x > r.max.y)
+			do if (++p>pn) return pn-p0;
+			while (p->y-slope*p->x >= r.max.y);
+		else return p - p0;
+	} while ((fr=frac_outside(p-1,p,&r,slope)) == 1);
+	return (p - p0) + fr-1;
+}
+
+
+
+/*********************** Drawing frame and axis labels  ***********************/
+
+#define Nthous  7
+#define Len_thous  30			/* bound on strlen(thous_nam[i]) */
+char* thous_nam[Nthous] = {
+	"one", "thousand", "million", "billion",
+	"trillion", "quadrillion", "quintillion",
+};
+
+
+typedef struct lab_interval {
+	double sep;			/* separation between tick marks */
+	double unit;		/* power of 1000 divisor */
+	int logunit;		/* log base 1000 of of this divisor */
+	double off;			/* offset to subtract before dividing */
+} lab_interval;
+
+
+char* abbrev_num(double x, const lab_interval* iv)
+{
+	static char buf[16];
+	double dx = x - iv->off;
+	dx = iv->sep * floor(dx/iv->sep + .5);
+	sprintf(buf,"%g", dx/iv->unit);
+	return buf;
+}
+
+
+double lead_digits(double n, double r)	/* n truncated to power of 10 above r */
+{
+	double rr = pow(10, ceil(log10(r)));
+	double nn = (n<rr) ? 0.0 : rr*floor(n/rr);
+	if (n+r-nn >= digs10pow) {
+		rr /= 10;
+		nn = (n<rr) ? 0.0 : rr*floor(n/rr);
+	}
+	return nn;
+}
+
+
+lab_interval next_larger(double s0, double xlo, double xhi)
+{
+	double nlo, nhi;
+	lab_interval r;
+	r.logunit = (int) floor(log10(s0) + LOG2);
+	r.unit = pow(10, r.logunit);
+	nlo = xlo/r.unit;
+	nhi = xhi/r.unit;
+	if (nhi >= digs10pow)
+		r.off = r.unit*lead_digits(nlo, nhi-nlo);
+	else if (nlo <= -digs10pow)
+		r.off = -r.unit*lead_digits(-nhi, nhi-nlo);
+	else	r.off = 0;
+	r.sep = (s0<=r.unit) ? r.unit : (s0<2*r.unit ? 2*r.unit : 5*r.unit);
+	switch (r.logunit%3) {
+	case 1:	r.unit*=.1; r.logunit--;
+		break;
+	case -1: case 2:
+		r.unit*=10; r.logunit++;
+		break;
+	case -2: r.unit*=100; r.logunit+=2;
+	}
+	r.logunit /= 3;
+	return r;
+}
+
+
+double min_hsep(const transform* tr)
+{
+	double s = (2+labdigs)*sdigit.x;
+	double ss = (univ.disp.min.x<0) ? s+sdigit.x : s;
+	return dxuntransform(tr, ss);
+}
+
+
+lab_interval mark_x_axis(const transform* tr)
+{
+	fpoint p = univ.disp.min;
+	Point q, qtop, qbot, tmp;
+	double x0=univ.disp.min.x, x1=univ.disp.max.x;
+	double seps0, nseps, seps;
+	lab_interval iv = next_larger(min_hsep(tr), x0, x1);
+	set_unslanted_y(&univ, &p.y, 0);
+	q.y = ytransform(tr, p.y) + .5;
+	qtop.y = q.y - tick_len;
+	qbot.y = q.y + framewd + framesep;
+	seps0 = ceil(x0/iv.sep);
+	for (seps=0, nseps=floor(x1/iv.sep)-seps0; seps<=nseps; seps+=1) {
+		char* num = abbrev_num((p.x=iv.sep*(seps0+seps)), &iv);
+		Font* f = display->defaultfont;
+		q.x = qtop.x = qbot.x = xtransform(tr, p.x);
+		line(screen, qtop, q, Enddisc, Enddisc, 0, axis_color, q);
+		tmp = stringsize(f, num);
+		qbot.x -= tmp.x/2;
+		string(screen, qbot, display->black, qbot, f, num);
+	}
+	return iv;
+}
+
+
+lab_interval mark_y_axis(const transform* tr)
+{
+	Font* f = display->defaultfont;
+	fpoint p = univ.disp.min;
+	Point q, qrt, qlft;
+	double y0, y1, seps0, nseps, seps;
+	lab_interval iv;
+	set_unslanted_y(&univ, &y0, &y1);
+	iv = next_larger(dyuntransform(tr,-f->height), y0, y1);
+	q.x = xtransform(tr, p.x) - .5;
+	qrt.x = q.x + tick_len;
+	qlft.x = q.x - (framewd + framesep);
+	seps0 = ceil(y0/iv.sep);
+	for (seps=0, nseps=floor(y1/iv.sep)-seps0; seps<=nseps; seps+=1) {
+		char* num = abbrev_num((p.y=iv.sep*(seps0+seps)), &iv);
+		Point qq = stringsize(f, num);
+		q.y = qrt.y = qlft.y = ytransform(tr, p.y);
+		line(screen, qrt, q, Enddisc, Enddisc, 0, axis_color, q);
+		qq.x = qlft.x - qq.x;
+		qq.y = qlft.y - qq.y/2;
+		string(screen, qq, display->black, qq, f, num);
+	}
+	return iv;
+}
+
+
+void lab_iv_info(const lab_interval *iv, double slant, char* buf, int *n)
+{
+	if (iv->off > 0)
+		(*n) += sprintf(buf+*n,"-%.12g",iv->off);
+	else if (iv->off < 0)
+		(*n) += sprintf(buf+*n,"+%.12g",-iv->off);
+	if (slant>0)
+		(*n) += sprintf(buf+*n,"-%.6gx", slant);
+	else if (slant<0)
+		(*n) += sprintf(buf+*n,"+%.6gx", -slant);
+	if (abs(iv->logunit) >= Nthous)
+		(*n) += sprintf(buf+*n," in 1e%d units", 3*iv->logunit);
+	else if (iv->logunit > 0)
+		(*n) += sprintf(buf+*n," in %ss", thous_nam[iv->logunit]);
+	else if (iv->logunit < 0)
+		(*n) += sprintf(buf+*n," in %sths", thous_nam[-iv->logunit]);
+}
+
+
+void draw_xy_ranges(const lab_interval *xiv, const lab_interval *yiv)
+{
+	Point p;
+	char buf[2*(19+Len_thous+8)+50];
+	int bufn = 0;
+	buf[bufn++] = 'x';
+	lab_iv_info(xiv, 0, buf, &bufn);
+	bufn += sprintf(buf+bufn, "; y");
+	lab_iv_info(yiv, u_slant_amt(&univ), buf, &bufn);
+	buf[bufn] = '\0';
+	p = stringsize(display->defaultfont, buf);
+	top_left = screen->r.min.x + lft_border;
+	p.x = top_right = screen->r.max.x - rt_border - p.x;
+	p.y = screen->r.min.y + outersep;
+	string(screen, p, display->black, p, display->defaultfont, buf);
+}
+
+
+transform draw_frame(void)
+{
+	lab_interval x_iv, y_iv;
+	transform tr;
+	Rectangle r = screen->r;
+	lft_border = (univ.disp.min.y<0) ? lft_border0+sdigit.x : lft_border0;
+	tr = cur_trans();
+	r.min.x += lft_border;
+	r.min.y += top_border;
+	r.max.x -= rt_border;
+	r.max.y -= bot_border;
+	border(screen, r, -framewd, axis_color, r.min);
+	x_iv = mark_x_axis(&tr);
+	y_iv = mark_y_axis(&tr);
+	draw_xy_ranges(&x_iv, &y_iv);
+	return tr;
+}
+
+
+
+/*************************** Finding the selection  ***************************/
+
+typedef struct pt_on_fpoly {
+	fpoint p;			/* the point */
+	fpolygon* fp;			/* the fpolygon it lies on */
+	double t;			/* how many knots from the beginning */
+} pt_on_fpoly;
+
+
+static double myx, myy;
+#define mydist(p,o,sl,xwt,ywt)	(myx=(p).x-(o).x, myy=(p).y-sl*(p).x-(o).y,	\
+					xwt*myx*myx + ywt*myy*myy)
+
+/* At what fraction of the way from p0[0] to p0[1] is mydist(p,ctr,slant,xwt,ywt)
+   minimized?
+*/
+double closest_time(const fpoint* p0, const fpoint* ctr, double slant,
+		double xwt, double ywt)
+{
+	double p00y=p0[0].y-slant*p0[0].x, p01y=p0[1].y-slant*p0[1].x;
+	double dx=p0[1].x-p0[0].x, dy=p01y-p00y;
+	double x0=p0[0].x-ctr->x, y0=p00y-ctr->y;
+	double bot = xwt*dx*dx + ywt*dy*dy;
+	if (bot==0)
+		return 0;
+	return -(xwt*x0*dx + ywt*y0*dy)/bot;
+}
+
+
+/* Scan the polygonal path of length len knots starting at p0, and find the
+   point that the transformation y=y-x*slant makes closest to the center of *r,
+   where *r itself defines the distance metric.  Knots get higher priority than
+   points between knots.  If psel->t is negative, always update *psel; otherwise
+   update *psel only if the scan can improve it.  Return a boolean that says
+   whether *psel was updated.
+     Note that *r is a very tiny rectangle (tiny when converted screen pixels)
+   such that anything in *r is considered close enough to match the mouse click.
+   The purpose of this routine is to be careful in case there is a lot of hidden
+   detail in the tiny rectangle *r.
+*/
+int improve_pt(fpoint* p0, double len, const frectangle* r, double slant,
+		pt_on_fpoly* psel)
+{
+	fpoint ctr = fcenter(r);
+	double x_wt=2/(r->max.x-r->min.x), y_wt=2/(r->max.y-r->min.y);
+	double xwt=x_wt*x_wt, ywt=y_wt*y_wt;
+	double d, dbest = (psel->t <0) ? 1e30 : mydist(psel->p,ctr,slant,xwt,ywt);
+	double tt, dbest0 = dbest;
+	fpoint pp;
+	int ilen = (int) len;
+	if (len==0 || ilen>0) {
+		int i;
+		for (i=(len==0 ? 0 : 1); i<=ilen; i++) {
+			d = mydist(p0[i], ctr, slant, xwt, ywt);
+			if (d < dbest)
+				{psel->p=p0[i]; psel->t=i; dbest=d;}
+		}
+		return (dbest < dbest0);
+	}
+	tt = closest_time(p0, &ctr, slant, xwt, ywt);
+	if (tt > len)
+		tt = len;
+	pp.x = p0[0].x + tt*(p0[1].x - p0[0].x);
+	pp.y = p0[0].y + tt*(p0[1].y - p0[0].y);
+	if (mydist(pp, ctr, slant, xwt, ywt) < dbest) {
+		psel->p = pp;
+		psel->t = tt;
+		return 1;
+	}
+	return 0;
+}
+
+
+/* Test *fp against *r after transforming by y=y-x*slope, and set *psel accordingly.
+*/
+void select_in_fpoly(fpolygon* fp, const frectangle* r, double slant,
+		pt_on_fpoly* psel)
+{
+	fpoint *p0=fp->p, *pn=fp->p+fp->n;
+	double l1, l2;
+	if (p0==pn)
+		{improve_pt(p0, 0, r, slant, psel); psel->fp=fp; return;}
+	while ((l1=out_length(p0,pn,*r,slant)) < pn-p0) {
+		fpoint p0sav;
+		int i1 = (int) l1;
+		p0+=i1; l1-=i1;
+		p0sav = *p0;
+		p0[0].x += l1*(p0[1].x - p0[0].x);
+		p0[0].y += l1*(p0[1].y - p0[0].y);
+		l2 = in_length(p0, pn, *r, slant);
+		if (improve_pt(p0, l2, r, slant, psel)) {
+			if (l1==0 && psel->t!=((int) psel->t)) {
+				psel->t = 0;
+				psel->p = *p0;
+			} else if (psel->t < 1)
+				psel->t += l1*(1 - psel->t);
+			psel->t += p0 - fp->p;
+			psel->fp = fp;
+		}
+		*p0 = p0sav;
+		p0 += (l2>0) ? ((int) ceil(l2)) : 1;
+	}
+}
+
+
+/* Test all the fpolygons against *r after transforming by y=y-x*slope, and return
+   the resulting selection, if any.
+*/
+pt_on_fpoly* select_in_univ(const frectangle* r, double slant)
+{
+	static pt_on_fpoly answ;
+	fpolygon* fp;
+	answ.t = -1;
+	for (fp=univ.p; fp!=0; fp=fp->link)
+		if (fintersects(r, &fp->bb, slant))
+			select_in_fpoly(fp, r, slant, &answ);
+	if (answ.t < 0)
+		return 0;
+	return &answ;
+}
+
+
+
+/**************************** Using the selection  ****************************/
+
+pt_on_fpoly cur_sel;			/* current selection if cur_sel.t>=0 */
+pt_on_fpoly prev_sel;			/* previous selection if prev_sel.t>=0 (for slant) */
+Image* sel_bkg = 0;			/* what's behind the red dot */
+
+
+void clear_txt(void)
+{
+	Rectangle r;
+	r.min = screen->r.min;
+	r.min.x += lft_border;
+	r.min.y += outersep;
+	r.max.x = top_left;
+	r.max.y = r.min.y + smaxch.y;
+	draw(screen, r, display->white, display->opaque, r.min);
+	top_left = r.min.x;
+}
+
+
+Rectangle sel_dot_box(const transform* tr)
+{
+	Point ctr;
+	Rectangle r;
+	if (tr==0)
+		ctr.x = ctr.y = Dotrad;
+	else	do_transform(&ctr, tr, &cur_sel.p);
+	r.min.x=ctr.x-Dotrad;  r.max.x=ctr.x+Dotrad+1;
+	r.min.y=ctr.y-Dotrad;  r.max.y=ctr.y+Dotrad+1;
+	return r;
+}
+
+
+void unselect(const transform* tr)
+{
+	transform tra;
+	if (sel_bkg==0)
+		sel_bkg = allocimage(display, sel_dot_box(0), CMAP8, 0, DWhite);
+	clear_txt();
+	if (cur_sel.t < 0)
+		return;
+	prev_sel = cur_sel;
+	if (tr==0)
+		{tra=cur_trans(); tr=&tra;}
+	draw(screen, sel_dot_box(tr), sel_bkg, display->opaque, ZP);
+	cur_sel.t = -1;
+}
+
+
+/* Text at top right is written first and this low-level routine clobbers it if
+   the new top-left text would overwrite it.  However, users of this routine should
+   try to keep the new text short enough to avoid this.
+*/
+void show_mytext(char* msg)
+{
+	Point tmp, pt = screen->r.min;
+	int siz;
+	tmp = stringsize(display->defaultfont, msg);
+	siz = tmp.x;
+	pt.x=top_left;  pt.y+=outersep;
+	if (top_left+siz > top_right) {
+		Rectangle r;
+		r.min.y = pt.y;
+		r.min.x = top_right;
+		r.max.y = r.min.y + smaxch.y;
+		r.max.x = top_left+siz;
+		draw(screen, r, display->white, display->opaque, r.min);
+		top_right = top_left+siz;
+	}
+	string(screen, pt, display->black, ZP, display->defaultfont, msg);
+	top_left += siz;
+}
+
+
+double rnd(double x, double tol)	/* round to enough digits for accuracy tol */
+{
+	double t = pow(10, floor(log10(tol)));
+	return t * floor(x/t + .5);
+}
+
+double t_tol(double xtol, double ytol)
+{
+	int t = (int) floor(cur_sel.t);
+	fpoint* p = cur_sel.fp->p;
+	double dx, dy;
+	if (t==cur_sel.t)
+		return 1;
+	dx = fabs(p[t+1].x - p[t].x);
+	dy = fabs(p[t+1].y - p[t].y);
+	xtol /= (xtol>dx) ? xtol : dx;
+	ytol /= (ytol>dy) ? ytol : dy;
+	return (xtol<ytol) ? xtol : ytol;
+}
+
+void say_where(const transform* tr)
+{
+	double xtol=dxuntransform(tr,1), ytol=dyuntransform(tr,-1);
+	char buf[100];
+	int n, nmax = (top_right - top_left)/smaxch.x;
+	if (nmax >= 100)
+		nmax = 100-1;
+	n = sprintf(buf,"(%.14g,%.14g) at t=%.14g",
+			rnd(cur_sel.p.x,xtol), rnd(cur_sel.p.y,ytol),
+			rnd(cur_sel.t, t_tol(xtol,ytol)));
+	if (cur_sel.fp->nam[0] != 0)
+		sprintf(buf+n," %.*s", nmax-n-1, cur_sel.fp->nam);
+	show_mytext(buf);
+}
+
+
+void reselect(const transform* tr)	/* uselect(); set cur_sel; call this */
+{
+	Point pt2, pt3;
+	fpoint p2;
+	transform tra;
+	if (cur_sel.t < 0)
+		return;
+	if (tr==0)
+		{tra=cur_trans(); tr=&tra;}
+	do_transform(&p2, tr, &cur_sel.p);
+	if (fabs(p2.x)+fabs(p2.y)>1e8 || (pt2.x=p2.x, pt2.y=p2.y, is_off_screen(pt2)))
+		{cur_sel.t= -1; return;}
+	pt3.x=pt2.x-Dotrad;  pt3.y=pt2.y-Dotrad;
+	draw(sel_bkg, sel_dot_box(0), screen, display->opaque, pt3);
+	fillellipse(screen, pt2, Dotrad, Dotrad, clr_im(DRed), pt2);
+	say_where(tr);
+}
+
+
+void do_select(Point pt)
+{
+	transform tr = cur_trans();
+	fpoint pt1, pt2, ctr;
+	frectangle r;
+	double slant;
+	pt_on_fpoly* psel;
+	unselect(&tr);
+	do_untransform(&ctr, &tr, &pt);
+	pt1.x=pt.x-fuzz;  pt1.y=pt.y+fuzz;
+	pt2.x=pt.x+fuzz;  pt2.y=pt.y-fuzz;
+	do_untransform(&r.min, &tr, &pt1);
+	do_untransform(&r.max, &tr, &pt2);
+	slant = u_slant_amt(&univ);
+	slant_frect(&r, -slant);
+	psel = select_in_univ(&r, slant);
+	if (psel==0)
+		return;
+	if (logfil!=0) {
+		fprintf(logfil,"%.14g\t%.14g\n", psel->p.x, psel->p.y);
+		fflush(logfil);
+	}
+	cur_sel = *psel;
+	reselect(&tr);
+}
+
+
+/***************************** Prompting for text *****************************/
+
+void unshow_mytext(char* msg)
+{
+	Rectangle r;
+	Point siz = stringsize(display->defaultfont, msg);
+	top_left -= siz.x;
+	r.min.y = screen->r.min.y + outersep;
+	r.min.x = top_left;
+	r.max.y = r.min.y + siz.y;
+	r.max.x = r.min.x + siz.x;
+	draw(screen, r, display->white, display->opaque, r.min);
+}
+
+
+/* Show the given prompt and read a line of user input.  The text appears at the
+   top left.  If it runs into the top right text, we stop echoing but let the user
+   continue typing blind if he wants to.
+*/
+char* prompt_text(char* prompt)
+{
+	static char buf[200];
+	int n0, n=0, nshown=0;
+	Rune c;
+	unselect(0);
+	show_mytext(prompt);
+	while (n<200-1-UTFmax && (c=ekbd())!='\n') {
+		if (c=='\b') {
+			buf[n] = 0;
+			if (n > 0)
+				do n--;
+				while (n>0 && (buf[n-1]&0xc0)==0x80);
+			if (n < nshown)
+				{unshow_mytext(buf+n); nshown=n;}
+		} else {
+			n0 = n;
+			n += runetochar(buf+n, &c);
+			buf[n] = 0;
+			if (nshown==n0 && top_right-top_left >= smaxch.x)
+				{show_mytext(buf+n0); nshown=n;}
+		}
+	}
+	buf[n] = 0;
+	while (ecanmouse())
+		emouse();
+	return buf;
+}
+
+
+/**************************** Redrawing the screen ****************************/
+
+/* Let p0 and its successors define a piecewise-linear function of a paramter t,
+   and draw the 0<=t<=n1 portion using transform *tr.
+*/
+void draw_fpts(const fpoint* p0, double n1, const transform* tr, int thick,
+		Image* clr)
+{
+	int n = (int) n1;
+	const fpoint* p = p0 + n;
+	fpoint pp;
+	Point qq, q;
+	if (n1 > n) {
+		pp.x = p[0].x + (n1-n)*(p[1].x - p[0].x);
+		pp.y = p[0].y + (n1-n)*(p[1].y - p[0].y);
+	} else	pp = *p--;
+	do_transform(&qq, tr, &pp);
+	if (n1==0)
+		fillellipse(screen, qq, 1+thick, 1+thick, clr, qq);
+	for (; p>=p0; p--) {
+		do_transform(&q, tr, p);
+		line(screen, qq, q, Enddisc, Enddisc, thick, clr, qq);
+		qq = q;
+	}
+}
+
+void draw_1fpoly(const fpolygon* fp, const transform* tr, Image* clr,
+		const frectangle *udisp, double slant)
+{
+	fpoint *p0=fp->p, *pn=fp->p+fp->n;
+	double l1, l2;
+	if (p0==pn && fcontains(udisp,*p0))
+		{draw_fpts(p0, 0, tr, fp->thick, clr); return;}
+	while ((l1=out_length(p0,pn,*udisp,slant)) < pn-p0) {
+		fpoint p0sav;
+		int i1 = (int) l1;
+		p0+=i1; l1-=i1;
+		p0sav = *p0;
+		p0[0].x += l1*(p0[1].x - p0[0].x);
+		p0[0].y += l1*(p0[1].y - p0[0].y);
+		l2 = in_length(p0, pn, *udisp, slant);
+		draw_fpts(p0, l2, tr, fp->thick, clr);
+		*p0 = p0sav;
+		p0 += (l2>0) ? ((int) ceil(l2)) : 1;
+	}
+}
+
+
+double get_clip_data(const fpolygons *u, frectangle *r)
+{
+	double slant = set_unslanted_y(u, &r->min.y, &r->max.y);
+	r->min.x = u->disp.min.x;
+	r->max.x = u->disp.max.x;
+	return slant;
+}
+
+
+void draw_fpoly(const fpolygon* fp, const transform* tr, Image* clr)
+{
+	frectangle r;
+	double slant = get_clip_data(&univ, &r);
+	draw_1fpoly(fp, tr, clr, &r, slant);
+}
+
+
+void eresized(int new)
+{
+	transform tr;
+	fpolygon* fp;
+	frectangle clipr;
+	double slant;
+	if(new && getwindow(display, Refmesg) < 0) {
+		fprintf(stderr,"can't reattach to window\n");
+		exits("reshap");
+	}
+	draw(screen, screen->r, display->white, display->opaque, screen->r.min);
+	tr = draw_frame();
+	slant = get_clip_data(&univ, &clipr);
+	for (fp=univ.p; fp!=0; fp=fp->link)
+		if (fintersects(&clipr, &fp->bb, slant))
+			draw_1fpoly(fp, &tr, fp->clr, &clipr, slant);
+	reselect(0);
+	if (mv_bkgd!=0 && mv_bkgd->repl==0) {
+		freeimage(mv_bkgd);
+		mv_bkgd = display->white;
+	}
+	flushimage(display, 1);
+}
+
+
+
+
+/********************************* Recoloring *********************************/
+
+int draw_palette(int n)		/* n is number of colors; returns patch dy */
+{
+	int y0 = screen->r.min.y + top_border;
+	int dy = (screen->r.max.y - bot_border - y0)/n;
+	Rectangle r;
+	int i;
+	r.min.y = y0;
+	r.min.x = screen->r.max.x - rt_border + framewd;
+	r.max.y = y0 + dy;
+	r.max.x = screen->r.max.x;
+	for (i=0; i<n; i++) {
+		draw(screen, r, clrtab[i].im, display->opaque, r.min);
+		r.min.y = r.max.y;
+		r.max.y += dy;
+	}
+	return dy;
+}
+
+
+Image* palette_color(Point pt, int dy, int n)
+{				/* mouse at pt, patch size dy, n colors */
+	int yy;
+	if (screen->r.max.x - pt.x > rt_border - framewd)
+		return 0;
+	yy = pt.y - (screen->r.min.y + top_border);
+	if (yy<0 || yy>=n*dy)
+		return 0;
+	return clrtab[yy/dy].im;
+}
+
+
+void all_set_clr(fpolygons* fps, Image* clr)
+{
+	fpolygon* p;
+	for (p=fps->p; p!=0; p=p->link)
+		p->clr = clr;
+}
+	
+
+void do_recolor(int but, Mouse* m, int alluniv)
+{
+	int nclr = clr_id(DWhite);
+	int dy = draw_palette(nclr);
+	Image* clr;
+	if (!get_1click(but, m, 0)) {
+		eresized(0);
+		return;
+	}
+	clr = palette_color(m->xy, dy, nclr);
+	if (clr != 0) {
+		if (alluniv)
+			all_set_clr(&univ, clr);
+		else	cur_sel.fp->clr = clr;
+	}
+	eresized(0);
+	lift_button(but, m, Never);
+}
+
+
+/****************************** Move and rotate  ******************************/
+
+void prepare_mv(const fpolygon* fp)
+{
+	Rectangle r = screen->r;
+	Image* scr0;
+	int dt = 1 + fp->thick;
+	r.min.x+=lft_border-dt;  r.min.y+=top_border-dt;
+	r.max.x-=rt_border-dt;   r.max.y-=bot_border-dt;
+	if (mv_bkgd!=0 && mv_bkgd->repl==0)
+		freeimage(mv_bkgd);
+	mv_bkgd = allocimage(display, r, CMAP8, 0, DNofill);
+	if (mv_bkgd==0)
+		mv_bkgd = display->white;
+	else {	transform tr = cur_trans();
+		draw(mv_bkgd, r, screen, display->opaque, r.min);
+		draw(mv_bkgd, sel_dot_box(&tr), sel_bkg, display->opaque, ZP);
+		scr0 = screen;
+		screen = mv_bkgd;
+		draw_fpoly(fp, &tr, display->white);
+		screen = scr0;
+	}
+}
+
+
+void move_fp(fpolygon* fp, double dx, double dy)
+{
+	fpoint *p, *pn=fp->p+fp->n;
+	for (p=fp->p; p<=pn; p++) {
+		(p->x) += dx;
+		(p->y) += dy;
+	}
+	(fp->bb.min.x)+=dx;  (fp->bb.min.y)+=dy;
+	(fp->bb.max.x)+=dx;  (fp->bb.max.y)+=dy;
+}
+
+
+void rotate_fp(fpolygon* fp, fpoint o, double theta)
+{
+	double s=sin(theta), c=cos(theta);
+	fpoint *p, *pn=fp->p+fp->n;
+	for (p=fp->p; p<=pn; p++) {
+		double x=p->x-o.x, y=p->y-o.y;
+		(p->x) = o.x + c*x - s*y;
+		(p->y) = o.y + s*x + c*y;
+	}
+	set_fbb(fp);
+}
+
+
+/* Move the selected fpolygon so the selected point tracks the mouse, and return
+   the total amount of movement.  Button but has already been held down for at
+   least Mv_delay milliseconds and the mouse might have moved some distance.
+*/
+fpoint do_move(int but, Mouse* m)
+{
+	transform tr = cur_trans();
+	int bbit = Button_bit(but);
+	fpolygon* fp = cur_sel.fp;
+	fpoint loc, loc0=cur_sel.p;
+	double tsav = cur_sel.t;
+	unselect(&tr);
+	do {	latest_mouse(but, m);
+		(fp->thick)++;		/* line() DISAGREES WITH ITSELF */
+		draw_fpoly(fp, &tr, mv_bkgd);
+		(fp->thick)--;
+		do_untransform(&loc, &tr, &m->xy);
+		move_fp(fp, loc.x-cur_sel.p.x, loc.y-cur_sel.p.y);
+		cur_sel.p = loc;
+		draw_fpoly(fp, &tr, fp->clr);
+	} while (m->buttons & bbit);
+	cur_sel.t = tsav;
+	reselect(&tr);
+	loc.x -= loc0.x;
+	loc.y -= loc0.y;
+	return loc;
+}
+
+
+double dir_angle(const Point* pt, const transform* tr)
+{
+	fpoint p;
+	double dy, dx;
+	do_untransform(&p, tr, pt);
+	dy=p.y-cur_sel.p.y;  dx=p.x-cur_sel.p.x;
+	return (dx==0 && dy==0) ? 0.0 : atan2(dy, dx);
+}
+
+
+/* Rotate the selected fpolygon around the selection point so as to track the
+   direction angle from the selected point to m->xy.  Stop when button but goes
+   up and return the total amount of rotation in radians.
+*/
+double do_rotate(int but, Mouse* m)
+{
+	transform tr = cur_trans();
+	int bbit = Button_bit(but);
+	fpolygon* fp = cur_sel.fp;
+	double theta0 = dir_angle(&m->xy, &tr);
+	double th, theta = theta0;
+	do {	latest_mouse(but, m);
+		(fp->thick)++;		/* line() DISAGREES WITH ITSELF */
+		draw_fpoly(fp, &tr, mv_bkgd);
+		(fp->thick)--;
+		th = dir_angle(&m->xy, &tr);
+		rotate_fp(fp, cur_sel.p, th-theta);
+		theta = th;
+		draw_fpoly(fp, &tr, fp->clr);
+	} while (m->buttons & bbit);
+	unselect(&tr);
+	cur_sel = prev_sel;
+	reselect(&tr);
+	return theta - theta0;
+}
+
+
+
+/********************************* Edit menu  *********************************/
+
+typedef enum e_index {
+		Erecolor, Ethick, Edelete, Eundo, Erotate, Eoptions,
+		Emove
+} e_index;
+
+char* e_items[Eoptions+1];
+
+Menu e_menu = {e_items, 0, 0};
+
+
+typedef struct e_action {
+	e_index typ;			/* What type of action */
+	fpolygon* fp;			/* fpolygon the action applies to */
+	Image* clr;			/* color to use if typ==Erecolor */
+	double amt;			/* rotation angle or line thickness */
+	fpoint pt;			/* movement vector or rotation center */
+	struct e_action* link;		/* next in a stack */
+} e_action;
+
+e_action* unact = 0;			/* heads a linked list of actions */
+e_action* do_undo(e_action*);		/* pop off an e_action and (un)do it */
+e_action* save_act(e_action*,e_index);	/* append new e_action for status quo */
+
+
+void save_mv(fpoint movement)
+{
+	unact = save_act(unact, Emove);
+	unact->pt = movement;
+}
+
+
+void init_e_menu(void)
+{
+	char* u = "can't undo";
+	e_items[Erecolor] = "recolor";
+	e_items[Edelete] = "delete";
+	e_items[Erotate] = "rotate";
+	e_items[Eoptions-cantmv] = 0;
+	e_items[Ethick] = (cur_sel.fp->thick >0) ? "thin" : "thick";
+	if (unact!=0)
+		switch (unact->typ) {
+		case Erecolor: u="uncolor"; break;
+		case Ethick: u=(unact->fp->thick==0) ? "unthin" : "unthicken";
+			break;
+		case Edelete: u="undelete"; break;
+		case Emove: u="unmove"; break;
+		case Erotate: u="unrotate"; break;
+		}
+	e_items[Eundo] = u;
+}
+
+
+void do_emenu(int but, Mouse* m)
+{
+	int h;
+	if (cur_sel.t < 0)
+		return;
+	init_e_menu();
+	h = emenuhit(but, m, &e_menu);
+	switch(h) {
+	case Ethick: unact = save_act(unact, h);
+		cur_sel.fp->thick ^= 1;
+		eresized(0);
+		break;
+	case Edelete: unact = save_act(unact, h);
+		fp_remove(&univ, cur_sel.fp);
+		unselect(0);
+		eresized(0);
+		break;
+	case Erecolor: unact = save_act(unact, h);
+		do_recolor(but, m, 0);
+		break;
+	case Erotate: unact = save_act(unact, h);
+		prepare_mv(cur_sel.fp);
+		if (get_1click(but, m, 0)) {
+			unact->pt = cur_sel.p;
+			unact->amt = do_rotate(but, m);
+		}
+		break;
+	case Eundo: unact = do_undo(unact);
+		break;
+	}
+}
+
+
+
+/******************************* Undoing edits  *******************************/
+
+e_action* save_act(e_action* a0, e_index typ)
+{					/* append new e_action for status quo */
+	e_action* a = malloc(sizeof(e_action));
+	a->link = a0;
+	a->pt.x = a->pt.y = 0.0;
+	a->amt = cur_sel.fp->thick;
+	a->clr = cur_sel.fp->clr;
+	a->fp = cur_sel.fp;
+	a->typ = typ;
+	return a;
+}
+
+
+/* This would be trivial except it's nice to preserve the selection in order to make
+   it easy to undo a series of moves.  (There's no do_unrotate() because it's harder
+   and less important to preserve the selection in that case.)
+*/
+void do_unmove(e_action* a)
+{
+	double tsav = cur_sel.t;
+	unselect(0);
+	move_fp(a->fp, -a->pt.x, -a->pt.y);
+	if (a->fp == cur_sel.fp) {
+		cur_sel.p.x -= a->pt.x;
+		cur_sel.p.y -= a->pt.y;
+	}
+	cur_sel.t = tsav;
+	reselect(0);
+}
+
+
+e_action* do_undo(e_action* a0)		/* pop off an e_action and (un)do it */
+{
+	e_action* a = a0;
+	if (a==0)
+		return 0;
+	switch(a->typ) {
+	case Ethick: a->fp->thick = a->amt;
+		eresized(0);
+		break;
+	case Erecolor: a->fp->clr = a->clr;
+		eresized(0);
+		break;
+	case Edelete: 
+		a->fp->link = univ.p;
+		univ.p = a->fp;
+		grow_bb(&univ.bb, &a->fp->bb);
+		eresized(0);
+		break;
+	case Emove:
+		do_unmove(a);
+		eresized(0);
+		break;
+	case Erotate:
+		unselect(0);
+		rotate_fp(a->fp, a->pt, -a->amt);
+		eresized(0);
+		break;
+	}
+	a0 = a->link;
+	free(a);
+	return a0;
+}
+
+
+
+/********************************* Main menu  *********************************/
+
+enum m_index {     Mzoom_in,  Mzoom_out,  Munzoom,  Mslant,    Munslant,
+		Msquare_up,  Mrecenter,  Mrecolor,  Mrestack,  Mread,
+		Mwrite,      Mexit};
+char* m_items[] = {"zoom in", "zoom out", "unzoom", "slant",   "unslant",
+		"square up", "recenter", "recolor", "restack", "read",
+		"write",     "exit", 0};
+
+Menu m_menu = {m_items, 0, 0};
+
+
+void do_mmenu(int but, Mouse* m)
+{
+	int e, h = emenuhit(but, m, &m_menu);
+	switch (h) {
+	case Mzoom_in:
+		disp_zoomin(egetrect(but,m));
+		eresized(0);
+		break;
+	case Mzoom_out:
+		disp_zoomout(egetrect(but,m));
+		eresized(0);
+		break;
+	case Msquare_up:
+		disp_squareup();
+		eresized(0);
+		break;
+	case Munzoom:
+		init_disp();
+		eresized(0);
+		break;
+	case Mrecenter:
+		if (get_1click(but, m, &bullseye)) {
+			recenter_disp(m->xy);
+			eresized(0);
+			lift_button(but, m, Never);
+		}
+		break;
+	case Mslant:
+		if (cur_sel.t>=0 && prev_sel.t>=0) {
+			slant_disp(prev_sel.p, cur_sel.p);
+			eresized(0);
+		}
+		break;
+	case Munslant:
+		univ.slant_ht = univ.disp.max.y - univ.disp.min.y;
+		eresized(0);
+		break;
+	case Mrecolor:
+		do_recolor(but, m, 1);
+		break;
+	case Mrestack:
+		fps_invert(&univ);
+		eresized(0);
+		break;
+	case Mread:
+		e = doinput(prompt_text("File:"));
+		if (e==0)
+			eresized(0);
+		else if (e<0)
+			show_mytext(" - can't read");
+		else {
+			char ebuf[80];
+			snprintf(ebuf, 80, " - error line %d", e);
+			show_mytext(ebuf);
+		}
+		break;
+	case Mwrite:
+		if (!dooutput(prompt_text("File:")))
+			show_mytext(" - can't write");
+		break;
+	case Mexit:
+		exits("");
+	}
+}
+
+
+
+/****************************** Handling events  ******************************/
+
+void doevent(void)
+{
+	ulong etype;
+	int mobile;
+	ulong mvtime;
+	Event	ev;
+
+	etype = eread(Emouse|Ekeyboard, &ev);
+	if(etype & Emouse) {
+		if (ev.mouse.buttons & But1) {
+			do_select(ev.mouse.xy);
+			mvtime = Never;
+			mobile = !cantmv && cur_sel.t>=0;
+			if (mobile) {
+				mvtime = ev.mouse.msec + Mv_delay;
+				prepare_mv(cur_sel.fp);
+			}
+			if (!lift_button(1, &ev.mouse, mvtime) && mobile)
+				save_mv(do_move(1, &ev.mouse));
+		} else if (ev.mouse.buttons & But2)
+			do_emenu(2, &ev.mouse);
+		else if (ev.mouse.buttons & But3)
+			do_mmenu(3, &ev.mouse);
+	}
+	/* no need to check (etype & Ekeyboard)--there are no keyboard commands */
+}
+
+
+
+/******************************** Main program ********************************/
+
+extern char* argv0;
+
+void usage(void)
+{
+	int i;
+	fprintf(stderr,"Usage %s [options] [infile]\n", argv0);
+	fprintf(stderr,
+"option ::= -l logfile | -m\n"
+"\n"
+"Read a polygonal line graph in an ASCII format (one x y pair per line, delimited\n"
+"by spaces with a label after each polyline), and view it interactively.  Use\n"
+"standard input if no infile is specified.\n"
+"Option -l specifies a file in which to log the coordinates of each point selected.\n"
+"(Clicking a point with button one selects it and displays its coordinates and\n"
+"the label of its polylone.)  Option -m allows polylines to be moved and rotated.\n"
+"The polyline labels can use the following color names:"
+	);
+	for (i=0; clrtab[i].c!=DNofill; i++)
+		fprintf(stderr,"%s%8s", (i%8==0 ? "\n" : "  "), clrtab[i].nam);
+	fputc('\n', stderr);
+	exits("usage");
+}
+
+void main(int argc, char *argv[])
+{
+	int e;
+
+	ARGBEGIN {
+	case 'm': cantmv=0;
+		break;
+	case 'l': logfil = fopen(ARGF(),"w");
+		break;
+	default: usage();
+	} ARGEND
+
+	if(initdraw(0, 0, "gview") < 0)
+		exits("initdraw");
+	einit(Emouse|Ekeyboard);
+
+	e = doinput(*argv ? *argv : "-");
+	if (e < 0) {
+		fprintf(stderr,"Cannot read input file %s\n", *argv);
+		exits("no valid input file");
+	} else if (e > 0) {
+		fprintf(stderr,"Bad syntax at line %d in input file\n", e);
+		exits("bad syntax in input");
+	}
+	init_disp();
+	init_clrtab();
+	set_default_clrs(&univ, 0);
+	adjust_border(display->defaultfont);
+	cur_sel.t = prev_sel.t = -1;
+	eresized(0);
+	for(;;)
+		doevent();
+}