#!/usr/bin/perl -w
use strict;
my ($appname,$version)=qw[Meander 0.1];

# Done:
# fix zooming (?)
# avoid scientific notation for coords
# select multiple overlapping points
# discontinuous motifs
# closing display window brokenness


# TODO: 
# load/save dialogs
# editable angles/lengths
# documentation
# better avoidance of runaway calculations
# bounding box calculation (for clipping when zoomed)
# anti-aliased display
# printing/image export
# email client


# buttons:
# 1: select, delayed drag
# 2: insta-drag, no select
# 3: safe select/deselect, no drag

use constant TRUE=>1;
use constant FALSE=>!TRUE;

use constant pi=>4*atan2(1,1);

my %mfvars; # Makefile Vars
my $typemap; # dirty hack, see below

BEGIN {
    # ExtUtils::Depends knows how to find the typemaps and headers for the
    # Gtk2 module and the ones on which it depends (Glib, basically).
    # use it to get the INC, LIBS, and TYPEMAPS information we need to give
    # to Inline::C so we can use gtk2-perl functions and such.
    use ExtUtils::Depends;
    my $dep = ExtUtils::Depends->new ('Meander', 'Glib', 'Gtk2');
    %mfvars = $dep->get_makefile_vars;

    # Inline::C's manpage claims that it takes the same value for TYPEMAPS as
    # MakeMaker, but this is an evil lie --- it takes only a filename string,
    # whereas MakeMaker takes a string or an array of strings.  We'll have to
    # consolidate the many typemap files from the depends object into one to
    # appease the fickle Inline.  This bites.
    my %typemap_info;
    use strict;
    foreach my $f (@{$mfvars{TYPEMAPS}}) {
	open IN, $f or die "can't open $f: $!\n";
	my $cur = '';
	while (<IN>) {
	    if ($cur eq '') {
		next unless /^TYPEMAP/;
		$cur = 'TYPEMAP';
	    } elsif (/^(INPUT|OUTPUT)/) {
		$cur = $1;
	    } else {
		push @{$typemap_info{$cur}}, $_;
	    }
	}
	close IN;
    }
    use File::Temp ':mktemp';
    $typemap = mktemp ("/tmp/".$0."XXXXXX");
    open OUT, ">$typemap" or die "can't write temp typemap file: $!";
    print OUT "# generated by $0 from ".join(" ", @{$mfvars{TYPEMAPS}})."\n\n";
    foreach my $section (qw(TYPEMAP INPUT OUTPUT)) {
	print OUT "$section\n";
	foreach (@{$typemap_info{$section}}) {
	    print OUT $_;
	}
    }
    close OUT;
}
#END {
#    unlink $typemap;
#}

use Inline (C => Config =>
	CCFLAGS => '-O3',
	INC => $mfvars{INC},
	LIBS => $mfvars{LIBS},
	TYPEMAPS => $typemap, #$mfvars{TYPEMAPS}, # $%@^ Inline, see above
);
use Inline C => <<'EOF';

#include <gtk2perl.h>

#define POLL_TIMEOUT   250000 /* microseconds */
#define POLL_INTERVAL  50000  /* points */

#define THRESH 0.5

#define W (da->allocation.width)
#define H (da->allocation.height)

GtkWidget *da;
double *motif;
int mlen;

int busy = 0, aborted = 0;
int count;
int next_poll;
struct timeval start_time;
int timeout_elapsed;
int lines;
unsigned int connected;

#define BUFSIZE 1024
GdkPoint buf[BUFSIZE+1], *bufptr;

static void draw_points (void) {
    (lines?gdk_draw_lines:gdk_draw_points)
	(da->window,
	 da->style->fg_gc[GTK_WIDGET_STATE (da)],
	 buf, bufptr-buf);

    if (lines)
	*buf=*--bufptr;

    count+=bufptr-buf;

    if (count > next_poll)
    {
	next_poll = count + POLL_INTERVAL;

	if (!timeout_elapsed) {
	    struct timeval t;
	    int diff;
	    gettimeofday (&t, NULL);
	    diff = (t.tv_sec - start_time.tv_sec) * 1e6 + t.tv_usec - start_time.tv_usec;
	    if (diff > POLL_TIMEOUT) {
		timeout_elapsed = TRUE;
		printf ("got timeout\n");
	    }
	}

	if (timeout_elapsed)
	    while (gtk_events_pending())
		gtk_main_iteration();
    }

    bufptr=buf;
    if (lines)
	bufptr++;
}

static void flush (void) {
    if (!lines)
	return;
    
    if (bufptr!=buf) {
	draw_points();
	bufptr--;
    }
}
	
static void meander (double x, double y, double dx, double dy, int iter) {
    int i;
    double *ptr = motif;

    if (aborted)
	return;

    /* only valid when all lengths<1 */
    if ((x-dx*2<0 && x+dx*2<0 && x-dy*2<0 && x+dy*2<0) ||
	(y-dy*2<0 && y+dy*2<0 && y-dx*2<0 && y+dx*2<0) ||
	(x-dx*2>W && x+dx*2>W && x-dy*2>W && x+dy*2>W) ||
	(y-dy*2>H && y+dy*2>H && y-dx*2>H && y+dx*2>H)) {
	flush();
	return;
    }

    for (i=0; i<mlen; i++) {
	double xx=*ptr++;
	double yy=*ptr++;
	
	double xn = dx*xx+dy*yy;
	double yn = dy*xx-dx*yy;

	int c = (connected*2 >> i);
	
	if (c & 7) {
	    if (!iter || xn*xn+yn*yn<THRESH || !(c & 2)) {
		bufptr->x = x; bufptr->y = y;
		if (++bufptr == buf+BUFSIZE)
		draw_points();
	    } else {
		meander (x, y, xn, yn, iter-1);
	    }
	}

	if (!(c & 2))
	    flush();


	x += xn;
	y += yn;
    }
}

/* unfortunately, adding the "void" prototype confuses Inline::C */
int abort_draw ()
{
    if (!busy)
	return FALSE;

    aborted = TRUE;

    return TRUE;
}

int draw_meander (SV *points_ref, GtkWidget *area, int xstart, int ystart, int dx, int dy, int maxiter, int l, int c) {
    AV *points;
    int len;
    double *ptr;
    int i;

    if (busy)
	return -1;

    busy = TRUE;
    aborted = FALSE;
    
    gettimeofday (&start_time, NULL);

    timeout_elapsed = FALSE;

    if (!SvROK(points_ref))
	croak ("oops");

    points = (AV*)SvRV (points_ref);

    len = av_len (points)+1;
    ptr = motif = malloc (len * sizeof (double));

    if (!motif)
	croak ("out of memory");

    if (len & 1)
	croak ("oops");

    mlen = len/2;

    for (i=0; i<len; i++) {
	SV **val = av_fetch (points, i, 0);
#if 0
	if (!SvNOK (*val))
	    croak ("oops");
#endif
	*ptr++=(double)SvNV(*val);
    }

    da = area;

    bufptr = buf;
    count = 0;
    lines = l;
    connected = c;

    next_poll = POLL_INTERVAL;

    if (maxiter--)
	meander (xstart, ystart, dx, dy, maxiter);
    else {
	bufptr->x = xstart;
	bufptr->y = ystart;
	bufptr++;
    }
	
    free (motif);

    bufptr->x = xstart+dx;
    bufptr->y = ystart+dy;
    bufptr++;
    
    draw_points();

    busy = FALSE;

    if (aborted)
	return -2;

    return count;
}

EOF

use Gnome2;
use Gnome2::Canvas;
use Gtk2 '-init';
use Gtk2::GladeXML;

Gnome2::Program->init ($appname, $version);

my $glade=Gtk2::GladeXML->new("meander.glade");

$glade->signal_autoconnect_from_package("main");


my @points;
my @angles;
my @connected;

my ($dispwin,$iterations,$editwin, $dispcanvas, $editcanvas,
    $drawingarea, $xtext, $ytext, $angtext, $lentext, 
    $xbutton, $ybutton, $anglebutton, $lengthbutton,
    $pointtext, $insertbutton, $deletebutton, $conntoggle, $sidesmenu,
    $statusbar, $presets, $filesel, $about)=

    map $glade->get_widget($_), qw[dispwin iterations editwin
				   dispcanvas editcanvas
				   drawingarea x y angle length
				   xbutton ybutton anglebutton lengthbutton
				   point insert delete connected
				   sides statusbar presets
				   fileselection about
				   ];

my @presets;
my $presetmenu=Gtk2::Menu->new();

$presets->set_submenu($presetmenu);

{
    local $/="\n\n";
    while (<DATA>) {
	push @presets, $_;
	/^# (.*)/ or die $_;
	my $n=$_;
	my $item=Gtk2::MenuItem->new_with_label($1);
	$item->show;
	$presetmenu->add($item);
	$item->signal_connect('activate', sub { load ($n) });
    }
}

my $line_offset=0;
my $displine;
my $editline;
#my @editline;
my $dragging=0;
my $panning=0;
my @panoffset;
my $ignoreentry=0;
my $fix='';

my $iter;
my $base;
my $autoiter;

my @editpoints;
my $selected_point;

my $xorigin=300;
my $yorigin=250;
my $xscale=100;
my $yscale=100;

my $statuscontext=$statusbar->get_context_id('foo');

$drawingarea->add_events(qw[button-press-mask]);

init_points();

update_display();
draw_motif_editor();

Gtk2->main;

sub points_to_angles {
    my @points=@_;
    my @angles;
    my ($x,$y)=@{shift @points};
    my $ang=0;
    for my $point (@points) {
	my ($xx,$yy)=@{$point};
#	if ($xx eq '' || $yy eq '') {
#	    print STDERR Dumper [@_] ;
#	    exit;
#	}
				    
	my ($dx,$dy)=($yy-$y, $xx-$x);
	$ang=atan2($dx,$dy);#-$ang;
	push @angles, [$ang, sqrt($dx*$dx+$dy*$dy)];
	($x,$y)=($xx,$yy);
    }
    return @angles;
}

sub normalize {
    my @angles=@_;
    my ($x,$y)=(0,0);
    my $ang=0;

    for my $a (@angles) {
	my ($angle,$dist)=@{$a};
	$ang+=$angle;
	$x+=$dist*cos($ang);
	$y+=$dist*sin($ang);
    }

    return (sqrt($x*$x+$y*$y),atan2($y,$x));
}

sub angles_to_points {
    my ($scale,$theta,@angles)=@_;
    my @points=([0,0]);

    my ($x,$y)=(0,0);
    my $ang=-$theta;
    
    for my $a (@angles) {
	my ($angle,$dist)=@{$a};
	$ang+=$angle;
	$x+=$dist/$scale*cos($ang);
	$y+=$dist/$scale*sin($ang);
	push @points,[$x,$y];
    }
    return @points;
}

sub gtk_main_quit
{
    abort_draw();
    Gtk2->main_quit;
}

sub gtk_false { FALSE }
sub gtk_true { TRUE }

sub on_iterations_changed {
    my $i=$iterations->get_text;
    if ($i=~/^\d+$/) {
	$iter=$i;
	update_display();
    }
}

sub update_display {
	$drawingarea->window->invalidate_rect($drawingarea->allocation,1);
}
    

sub draw_motif_editor {
    $editline=Gnome2::Canvas::Item->new($editcanvas->root, "Gnome2::Canvas::Shape")
	unless $editline;

    my $path=Gnome2::Canvas::PathDef->new;

    for (map {[$points[$_][0],-$points[$_][1],$_&&$connected[$_-1]]}
    0..$#points) {
#	print join(',',@$_),"\n";
	if ($_->[2]) {
	    $path->lineto($_->[0],$_->[1]);
	} else {
	    $path->moveto($_->[0],$_->[1]);
	}
    }
    
    $editline->set('outline-color'=>'black',
		   'width-pixels'=>1
		   );
    $editline->set_path_def ($path);

    my $size=0.03;
    for my $i (0..$#points) {
	unless ($editpoints[$i]) {
	    $editpoints[$i]=Gnome2::Canvas::Item->new($editcanvas->root,
						      "Gnome2::Canvas::Rect");
	    $editpoints[$i]->signal_connect (event => \&on_select_point, $i);
#	    $editpoints[$i]->signal_connect (event => sub {die "foo: $i"}, $i);
	}
	    
	$editpoints[$i]->set(
			     x1 => $_->[0]-$size,
			     y1 => -$_->[1]-$size,
			     x2 => $_->[0]+$size,
			     y2 => -$_->[1]+$size,
			     fill_color => 'black',
			     ) for $points[$i];
    }
    for my $i (@points..$#editpoints) {
	next unless $editpoints[$i];
	$editpoints[$i]->destroy;
	undef $editpoints[$i];
    }

    do_select($selected_point);
}

sub fix_point {
    my ($x,$y)=@_;
    my ($oldx,$oldy)=@{$points[$selected_point]};
    +{ # DWIM, dammit!
	$xbutton => sub { $x=$oldx },
	$ybutton => sub { $y=$oldy },
	$anglebutton => sub {
	    my ($nx, $ny)=@{$points[$selected_point+$line_offset*2+1]};
	    $x-=$nx;$y-=$ny;
	    my $nlen=sqrt($x*$x+$y*$y);
	    my $ang=atan2($oldy-$ny,$oldx-$nx);
	    $ang+=pi if $x*($oldx-$nx)<0;
	    ($x,$y)=($nx+cos($ang)*$nlen,$ny+sin($ang)*$nlen);
	},
	$lengthbutton => sub { 
	    my ($ang, $len)=@{$angles[$selected_point+$line_offset]};
	    my ($nx, $ny)=@{$points[$selected_point+$line_offset*2+1]};
	    $x-=$nx;$y-=$ny;
	    my $nlen=sqrt($x*$x+$y*$y);
	    if ($nlen) {
		($x,$y)=($nx+$x*$len/$nlen, $ny+$y*$len/$nlen);
	    } else {
		($x,$y)=($oldx,$oldy);
	    }
	},
    }->{$fix}();

    return ($x,$y);
}

sub on_select_point {
    my ($item, $event, $num)=@_;
    undef $num if $item eq $editcanvas;
#    printf "%s: %s\n", $event->type, $num;
#    return 0 unless ref($event) eq 'Gtk2::Gdk::Event::Button';
    if ($event->type eq 'button-press') {
	do_select($num) if $event->button!=2 && defined $num;# || $event->button==3);
	if ($selected_point && $selected_point<$#points) {
	    if ($event->button==2) {
		my ($x, $y)=($event->x,$event->y);
		($x, $y)=$editcanvas->window_to_world($x, $y)
		    if $item eq $editcanvas; # WHY?
		$y=-$y;
		($x,$y)=fix_point($x,$y) if $fix;

		$points[$selected_point]=[$x,$y];
		update_angles();
		points_changed();
	    }
	    if ($event->button!=3) {
		$editpoints[$selected_point]->grab ([qw[pointer-motion-mask button-release-mask]],
						  Gtk2::Gdk::Cursor->new('fleur'), 0);
		$dragging=1;
	    }
	}
    } elsif ($event->type eq 'button-release') {
	$editpoints[$selected_point]->ungrab() if $dragging;
	$dragging=0;
    } elsif ($event->type eq 'motion-notify') {
	if ($dragging) {
	    die unless defined $selected_point;
	    my ($x, $y)=($event->x,-$event->y);
	    1e-15>abs and $_=0 for $x,$y; # $*@%ing rounding errors
	    ($x,$y)=fix_point($x,$y) if $fix;
	    $points[$selected_point]=[$x,$y];
	    update_angles();
	    points_changed();
	}
    }
    return defined $num?TRUE:FALSE;
}

sub do_select {
    $ignoreentry=1;
#    print Dumper $selected_point,@_;
    if (defined $selected_point) {
	$editpoints[$selected_point]->set(
					  fill_color => 'black',
					  );
#	printf "x1=%f\n", $editpoints[$selected_point]->get("x1");
    }
    my $i=$selected_point=shift;
    if (defined $i) {
	$editpoints[$i]->set(
					  fill_color => 'red',
					  );

	$_->[0]->set_text($_->[1])
	    for ([$xtext=>$points[$i][0]],
		 [$ytext=>$points[$i][1]],
		  );

	if ($i+$line_offset>=0 && $i+$line_offset<$#points) {
	    $angtext->set_text($angles[$i+$line_offset][0]*180/pi);
	    $lentext->set_text($angles[$i+$line_offset][1]);
	}
	$pointtext->set_value($i+1) unless $pointtext->get_value eq $i+1;
	$conntoggle->set_active($connected[$i+$line_offset]);
    }

    {
	my $e=$i && $i<$#points;
	for ($xtext,$ytext) {
	    $_->set_editable($e);
	}
    }

    {
	my $e=defined $i && ($i+$line_offset>=0) && ($i+$line_offset<$#points);
	for ($angtext,$lentext) {
	    $_->set_editable($e);
	    $_->set_text('') unless $e;
	}
    }

    $deletebutton->set_sensitive($i && $i<$#points);
    $insertbutton->set_sensitive(defined $i);
    $conntoggle->set_sensitive(defined $i && ($i+$line_offset>=0) && ($i+$line_offset<$#points));

    $ignoreentry=0;
}

sub on_show_display_window_activate {
    $dispwin->show;
}

sub points_changed {
    $pointtext->get('adjustment')->upper(scalar @points);
    draw_motif_editor();
    update_display();
}    

sub update_angles {
    @angles=points_to_angles(@points);
}

sub update_points {
    @points=angles_to_points(@angles);
}

sub on_xy_changed {
    return FALSE unless defined $selected_point;
    return FALSE if $ignoreentry;
    my ($obj)=@_;
    die unless $obj eq $xtext || $obj eq $ytext;
    my $coord=$obj->get_text;
    if ($coord=~/^[-+]?\d*.?\d+$/) {
	$points[$selected_point][($obj eq $xtext)?0:1]=$obj->get_text;
	update_angles();
	points_changed();
    }
    return FALSE;
}

sub on_insert_clicked {
    if (defined $selected_point) {
	my $i=$selected_point;
	$i-- if ($i==$#points);
	my ($x,$y);
	$x=($points[$i][0]+$points[$i+1][0])/2;
	$y=($points[$i][1]+$points[$i+1][1])/2;
	splice @points, $i+1, 0, [$x,$y];
	splice @connected, $i+1, 0, 1;#$connected[$i]&&$connected[$i+1];
	update_angles();
	do_select($i+1);
	points_changed();
    }
    return FALSE;
}

sub on_delete_clicked {
    if (defined $selected_point) {
	my $i=$selected_point;
	if ($i && $i<$#points) {
	    splice @points, $i, 1;
	    splice @connected, $i, 1;
	    update_angles();
	    do_select($i-1);
	    points_changed();
	}
#	$selected_point=undef;
    }
    return FALSE;
}

sub on_point_changed {
    do_select (shift->get_value-1);
    return FALSE;
}

sub on_connected_toggled {
    return FALSE unless defined $selected_point;
    my $new=shift->get_active;
    for ($connected[$selected_point+$line_offset]) {
	$_=$new,points_changed() if defined $_ && $_!=$new;
    }
    return FALSE;
}

sub init_points {
    @points=([0,0],[1,0]);
    @connected=(1);
    $base=1;
    $iter=4;
    update_angles();
    update_params();
    do_select(undef);
    points_changed();
    do_select(0);
}

sub load {
    my $f=shift;
    init_points();
    for (split /\n/, $f) {
	next if /^#/;
	chomp;
	$iter=$1, next if /^iterations\s+(\d+)/;
	$base=$1, next if /^baseline\s+(\d+)/;
	if (s/^points\s+//) {
	    @points=([0,0]);
	    @connected=(!!s/^\s*-//);
	    while (s/^\s*\(([-+]?\d*.?\d+)\s+([-+]?\d*.?\d+\s*)\)//) {
		push @points,[$1,$2];
		push @connected,!!s/^\s*-//;
	    }
	    push @points,[1,0];
	    next if /^\s*$/;
	}
	die "Syntax error: $_";
    }
    update_angles();
    update_params();
#    do_select(undef);
    points_changed();
    do_select(0);
}

sub save {
    sub conn { $connected[shift]?'-':' '}
    print <<"EOF";
# Generated by $appname $version
points @{[conn(0).join "",map { sprintf "(%s %s)%s", @{$points[$_]}, conn($_) } 1..$#points-1]}
baseline $base
iterations $iter
EOF
}
sub on_open1_activate {
    $filesel->show;
}

sub on_fileselection_response {
    my ($obj,$event)=@_;
}

sub update_params {
    $iterations->set_text($iter);
    $sidesmenu->set_text($base);
}

sub on_new1_activate {
    init_points();
}

sub on_save1_activate {
    save();
}

sub on_previous_line1_activate {
    $line_offset=-1;
    do_select($selected_point);
}

sub on_next_line1_activate {
    $line_offset=0;
    do_select($selected_point);
}

{
    my $ignore=0;

    sub set_fix {
	$fix=shift;
	$fix=0 unless $fix && $fix->get_active;
	$ignore=1;

	for ($xbutton, $ybutton, $lengthbutton, $anglebutton) {
	$_->set_active($_==$fix);

	$ignore=0;
    }
	# FIXME: radios
    }

    sub on_fixbutton_activate {
	return if $ignore;
	set_fix (shift);
    }
}

sub on_none1_activate {
    on_fixbutton_activate (FALSE);
}

sub on_x_position1_activate {
    on_fixbutton_activate ($xbutton);
}


sub on_y_position1_activate {
    on_fixbutton_activate ($ybutton);
}

sub on_angle1_activate {
    on_fixbutton_activate ($anglebutton);
}

sub on_length1_activate {
    on_fixbutton_activate ($lengthbutton);
}

sub on_sides_changed {
    my $s=$sidesmenu->get_text;
    if ($s=~/^(\d+)/ && $1) {
	$base=$1;
	update_display();
    }
}    

sub on_autoiter_toggled {
    my ($obj)=@_;
    $autoiter=$obj->get_active;
    $iterations->set_sensitive(!$autoiter);
    update_display();
}

sub on_drawingarea_click {
    my ($obj,$event)=@_;
    my $size=$drawingarea->allocation;
    if ($event->button==2) {
	$xorigin-=($event->x-$size->width/2);
	$yorigin-=($event->y-$size->height/2);
	update_display();
    } elsif ($event->button==1 || $event->button==3) {
	zoom($event->x,$event->y,$event->button==1);
    }
}

sub on_drawingarea_scroll_event {
    my ($obj,$event)=@_;
    zoom($event->x,$event->y,$event->direction eq 'up');
}

sub zoom {
    my ($x,$y,$dir)=@_;
    my $zoom=7/6;
    my $size=$drawingarea->allocation;
#    print "button: ".$event->button."\n";
    if ($dir) {
	$xscale*=$zoom;
	$yscale*=$zoom;
#	$xorigin+=$size->width*($zoom-1)/2;
#	$yorigin+=$size->height*($zoom-1)/2;
	$xorigin=$xorigin*$zoom-$x*($zoom-1);
	$yorigin=$yorigin*$zoom-$y*($zoom-1);
    } else {
	$xscale/=$zoom;
	$yscale/=$zoom;
	$xorigin=$xorigin/$zoom+$x*($zoom-1)/$zoom;
	$yorigin=$yorigin/$zoom+$y*($zoom-1)/$zoom;
#	$xorigin=($xorigin/$zoom+$event->x*(1/$zoom-1));
#	$yorigin=($yorigin/$zoom+$event->y*(1/$zoom-1));
    }
	update_display();
}

sub on_flip_motif1_activate {
    $_->[1]=-$_->[1] for @points;
    update_angles();
    points_changed();
}

sub on_about1_activate {
    $about->show;
}

sub hide_window {
    shift->hide;
    return TRUE;
}

sub on_aboutlogo_expose_event {
    my ($obj, $event)=@_;

    # this may fail if we're already busy drawing, but it's not important
    draw_meander ([.4, .2, .2, -.4, .4, .2],
			      $obj,
			      25, 40,
			      150, 0,
			      20, 0, -1);
}

{
    my $busy=FALSE;
    sub busy {
	return unless $busy!=$_[0];
	$busy = $_[0];
	$_ && $_->window->set_cursor($busy ? Gtk2::Gdk::Cursor->new("watch"):undef) for $dispwin,$editwin;
    }
}

sub set_status {
    $statusbar->pop($statuscontext);
    $statusbar->push($statuscontext, shift);
}

sub on_expose_event {
    my ($obj, $event)=@_;

    my $maxiter=$iter;
    if ($autoiter) {
	# check convergence
	my @a=map {$connected[$_]?$angles[$_]:()} 0..$#connected;
	if (@points>2 && grep { $_->[1]>=0.99 } @a) {
	    $maxiter=log(1e7)/log($#points);
	} else {
	    $maxiter=500;
	}
    }
    
    my $start=$base>2?pi/2+pi/$base:pi;
    my ($x,$y);

    my $count=0;
    my ($starttime)=times;
    my $c=0;
    $c|=($connected[$_]<<$_) for 0..$#points-1;

    busy(TRUE);

    if (abort_draw()) {

# we've been called when we were already in the middle of drawing; the
# snag is that we need to return in order to tell the previous
# operation to abort, and we can't start anything new until that's happened.
# so we have to make sure we get called again soon.

# this doesn't work because we get called with multiple rectangles and we'd
# need to queue them
#	my $rect=$event->area;
#	# we seem to need to clone this for the callback :(
#	$rect=Gtk2::Gdk::Rectangle->new($rect->x, $rect->y, $rect->width, $rect->height);
#	print "rect:", join(",", $rect->x, $rect->y, $rect->width, $rect->height), "\n";
#	Glib::Timeout->add (10,sub { $drawingarea->window->invalidate_rect ($rect, 1) });

# this works, but blanks the entire display, which is irritating
#	Glib::Timeout->add (10,sub { $drawingarea->window->invalidate_rect ($rect, 1) });

# best solution(?): don't bother invalidating, just draw unbuffered in real-time
	Glib::Timeout->add (10,\&on_expose_event,$obj); # ,$event?
	  return 0;
      }

    my $complete=1;
BASELOOP:
    for my $i (0..$base) {
#	$obj->modify_fg ($obj->state, Gtk2::Gdk::Color->new (65535*($i &1),32767*($i &2), 16383*($i &4)));
	my $xx=cos(2*pi*$i/$base+$start);
	my $yy=sin(2*pi*$i/$base+$start);
	$start=0 if $base==1; #ewwww
	if ($i) {
	    my $dx=$xx-$x;
	    my $dy=$yy-$y;

	    my @a=(0,0);
	    my $ret=draw_meander ([map {
		my @b=($_->[0]-$a[0], $_->[1]-$a[1]); @a=@$_; @b;
	    } @points[1..$#points]],
				  $obj,
				  $xorigin+$x*$xscale, $yorigin+$y*$yscale,
				  $dx*$xscale, $dy*$yscale,
				  $maxiter, !$autoiter,
				  $c
				  );
	    $complete=0, last BASELOOP if $ret<0;

	    $count+=$ret;
	}
	($x,$y)=($xx,$yy);
    }

    busy(FALSE);

    if ($complete) {
	set_status(sprintf "%d points, %.2fs elapsed", $count, times-$starttime);
    } else {
	set_status("Plotting...");
    }

    return FALSE;
}

__DATA__
# Lévy curve
points -(0.5 0.5)-
baseline 1
iterations 15

# Lévy square
points -(0.5 0.5)-
baseline 2
iterations 15

# Lévy square with warts
points -(0 0.5)-(0.5 0.5)-(0.5 0.33333333333)-(0.5 0.5)-(1 0.5)-
baseline 2
iterations 6

# Lévy tapestry
points -(0.5 -0.5)-
baseline 4
iterations 14

# Lévy hexagon
points -(0.25 0.43301270189221932338)-(0.75 0.43301270189221932338)-
baseline 2
iterations 8

# Koch snowflake
points -(0.3333333333333 0)-(0.5 0.28867513459481)-(0.66666666667 0)-
baseline 3
iterations 6

# Koch anti-snowflake
points -(0.3333333333333 0)-(0.5 -0.28867513459481)-(0.666666666667 0)-
baseline 3
iterations 6

# Minkowski sausage
points -(0.25 0)-(0.25 0.25)-(0.5 0.25)-(0.5 0)-(0.5 -0.25)-(0.75 -0.25)-(0.75 0)-
baseline 4
iterations 4

# Cross (Mandelbrot quintet)
points -(0.4 0.2)-(0.6 -0.2)-
baseline 4
iterations 5

# Square pattern
points -(0 0.5)-(0.5 0.5)-(0.5 0.0)-
baseline 4
iterations 6

# Square pattern 2
points -(0 0.5)-(0.5 0.5)-(0.5 0.0)-(0.57333333 -0.26)-
#(0.5 -0.24)
baseline 4
iterations 6

# Bushy square
points -(0.25 0.25)-(0.65849365 0.09150635)-(0.75 -0.25)-
baseline 4
iterations 5

# Dragon curve
points -(-0.5 0.5) (0.5 -0.5)-
baseline 1
iterations 16

# Twindragon
points -(0.5 0.5) (0.5 -0.5)-
baseline 1
iterations 16

# Terdragon
points -(0.5 -0.28867513459481)-(0.5 0.28867513459481)-
baseline 1
iterations 8

# Tergriffin
points -(0.5 0.28867513459481288224) (0.5 -0.28867513459481288224)-(1 0) (0.75 -0.14433756729740644111)-(0.25 0.14433756729740644111) 
baseline 1
iterations 9

# Quarter cross
points -(0.2 0.4)-(0.6 0.2)-(0.4 -0.2)-(0.8 -0.4)-
baseline 1
iterations 7

# Sierpinski gasket
points -(0.5 0)-(0.25 0.4330127018922)-(0.75 0.4330127018922)-(0.5 0)-
baseline 1
iterations 8

# Seirpinski gasket II
points -(0.5 0)-(0.25 -0.4330127018922) (0.5 0)-
baseline 1
iterations 7

# Sierpinski carpet
points -(0.3333333333 0) (0.6666666667 0)-(1 0) (1 0.3333333333)-(1 0.6666666667)-(1 1) (0.6666666667 1)-(0.3333333333 1)-(0 1) (0 0.6666666667)-(0 0.3333333333) (0 0) (0.3333333333 0)-(0.6666666667 0) 
baseline 1
iterations 5

# 0-isotrianguloid (Pseudo-Sierpinski triangle)
points -(0.5 0) (0.5 0.8660254037844)-(0.25 0.433012701892219) (0.75 0.1443375672974)-(0.5 0.57735026919) (0.5 -0)-
baseline 1
iterations 8

# 60-isotrianguloid
points -(0.25 -0.43301270189221932338) (0.75 -0.43301270189221932338)-(1 0) (0.75 0)-(0.25 0) (0.75 .43301270189221932338)-(0.25 .43301270189221932338) 
baseline 1
iterations 8

# 60-0-isotrianguloid
points -(0.25 -0.43301270189221932338) (0.75 -0.43301270189221932338)-(1 0) (0.25 0)-(0.75 0) (0.75 .43301270189221932338)-(0.25 .43301270189221932338) 
baseline 1
iterations 8

# Filled Koch snowflake
points -(0.3333333333333333 0) (0.1666666666666666667 0.2886751345948)-(0.5 0.2886751345948)-(0.833333333333333 0.2886751345948) (0.166666666666667 -0.2886751345948)-(0.5 -0.2886751345948)-(0.833333333333333 -0.2886751345948) (0.5 -0.2886751345948)-(0.5 0.2886751345948) (0.666666666666667 0)-
baseline 1
iterations 5

# 0-cyclohexamer
points -(0.3333333333333333 0) (0.1666666666666666667 0.2886751345948)-(0.5 0.2886751345948)-(0.833333333333333 0.2886751345948) (0.166666666666667 -0.2886751345948)-(0.5 -0.2886751345948)-(0.833333333333333 -0.2886751345948) (0.666666666666667 0)-
baseline 1
iterations 5

# Sierpinski snowflake
points -(0.3333333333333333 0)-(0.666666666666667 0)-(1 0) (0.1666666666666666667 0.2886751345948)-(0.5 0.2886751345948)-(0.833333333333333 0.2886751345948) (0.166666666666667 -0.2886751345948)-(0.5 -0.2886751345948)-(0.833333333333333 -0.2886751345948) 
baseline 1
iterations 5

# Frozen teardrop
points -(0.3333333333333333 0) (0.25 0.43301270189221932338)-(0.41666666666666666 0.14433756729740644111) (0.75 0.43301270189221932338)-(0.58333333333333333 0.14433756729740644111) (0.25 -0.43301270189221932338)-(0.416666666666666667 -0.14433756729740644111) (0.75 -0.43301270189221932338)-(0.583333333333333333333 -0.14433756729740644111) (1 0)-(1.33333333333333333 0) (1 0)-(0.6666666666666666667 0)-
baseline 1
iterations 6

# Fudgeflake
points -(0.5 0.2886751345948) (0.5 0.86602540378)-(0.5 0.2886751345948) (1 0)-(.5 0.2886751345948) 
baseline 1
iterations 8

# Star metatrapezium
points -(0.25 0)-(0.75 0)-(1 0) (0.25 0)-(0.375 .21650635094610966169)-(0.625 0.21650635094610966169)-(0.75 0)-(0.625 0.21650635094610966169)-(0.75 0.43301270189221932338)-(0.625 0.21650635094610966169)-(0.375 0.21650635094610966169)-(0.25 0.43301270189221932338)-(0.375 0.21650635094610966169)-(0.25 0) 
baseline 2
iterations 6

# Curved square
points -(0.5 0.05)-(0.5 -0.452493781056)-(0.5 0.05)-
baseline 4
iterations 8

# Torn square 
points -(0.4709 0)-(0.5 -0.47)-(0.5291 0)-
baseline 4
iterations 8

# Ice square
points -(0.5 0)-(0.5 -0.333333333333)-(0.5 0)-
baseline 4
iterations 8

# Ice triangle
points -(0.5 0)-(0.375 -0.2165063509461)-(0.5 0)-(0.625 -0.2165063509461)-(0.5 0)-
baseline 3
iterations 6

# Pentagram
points -(0.38196601125 0)-(0.5 -0.363271264)-(0.61803398875 0)-
baseline 5
iterations 6

# Sphinx
points -(0.25 0.43301270189)-(0.5 0.86602540378)-(0.75 0.43301270189)-
baseline 3
iterations 8

# Zigzag square
points -(0.25 0.25)-(0.125 0)-(0.875 0)-(0.75 -0.25)-
baseline 4
iterations 15

# Octagonal
points -(0.146447 0.353553)-(0.5 0.5)-(0.853553 0.353553)-(1 0)-(0.853553 -0.353553)-(0.5 -0.5)-(0.146447 -0.353553)-(0 0)
baseline 1
iterations 6

# Logarithmic spiral
points -(0.04 0.15333333)-
baseline 1
iterations 200

# Double spiral
points -(0.01 0.04)-(0.99 -0.04)-
baseline 1
iterations 800

# Alien brains
points -(0.47333333 0.32)-(1.01333333 0.00666667)-(0.55333333 -0.14)-
baseline 1
iterations 9

# Loopy
points -(0.52 -0.23333333)-(0.5 -0.45)-(0.48 -0.23333333)-
baseline 5
iterations 8

# Whirl
points -(0.3 0.2)-(0.5 0)-(0.22 0.22)-
baseline 3
iterations 12

# Monstrous
points -(0.4 0)-(0.5 0.08)-(1.16 -0.28)-
baseline 4
iterations 15

# Starry
points -(0.393333 0.36)-(0.58 -0.353333)-
baseline 1
iterations 10

# Swirly
points -(0.25 0)-(0.25 0.25)-(0.75 -0.25)-(0.75 0)-
baseline 5
iterations 8

# Swirly 2
points -(0.2 0.25)-(0.8 -0.25)-
baseline 4
iterations 30

# Swirly 3
points -(0.133333 0.166667)-(0.900000 -0.266667)-
baseline 6
iterations 40

# [Hexagonal]
points -(0.25 0.43301270189221932338)-(0.75 0.43301270189221932338)-(1 0)-(0.75 -0.43301270189221932338)-(0.25 -0.43301270189221932338)-(0 0)
baseline 1
iterations 5

# [Worm]
points -(0 0.25)-(0 0.50)-(0.25 0.5)-(0.5 0.5)-(0.5 0.25)-(0.5 0)-(0.5 -0.25)-(0.5 -0.5)-(0.75 -0.5)-(1 -0.5)-(1 -0.25)-
baseline 1
iterations 6

# Sheaf
points -(0.25 0)-(0.5 0)-(0.5 0.25)-(0.75 0.25)-(0.5 0.25)-(0.5 0)-(0.75 0)-
baseline 2
iterations 5

# Sheaf 2
points -(0.25 0.0)-(0.5 0.0)-(0.5 0.25)-(0.75 0.25)-(0.5 0.25)-(0.5 0.0)-(0.5 -0.25)-(0.25 -0.25)-(0.5 -0.25)-(0.5 0.0)-(0.75 0.0)-
baseline 4
iterations 5

# Anti-Koch 6 [dendritish]
points -(0.25 0)-(0.5 -.43301270189221932338)-(0.75 0)-
baseline 6
iterations 8

# Anti-Koch 3
# Generated by Meander 0.0.H
points -(0.25 0)-(0.5 -.43301270189221932338)-(0.75 0)-
baseline 3
iterations 8

# Anti-Koch II 3
points -(0.25 0)-(0.375 -0.21650635094611)-(0.5 0)-(0.625 -0.21650635094611)-(0.75 0)-
baseline 3
iterations 8

# Anti-Koch III 3
points -(0.25 0)-(0.375 -0.21650635094611)-(0.5 0)-(0.625 -0.21650635094611)-(0.5 -0.43301270189221932338)-(0.375 -0.21650635094611)-(0.625 -0.21650635094611)-(0.75 0)-
baseline 3
iterations 5

# Anti-Koch III 6
points -(0.25 0)-(0.375 -0.21650635094611)-(0.5 0)-(0.625 -0.21650635094611)-(0.5 -0.43301270189221932338)-(0.375 -0.21650635094611)-(0.625 -0.21650635094611)-(0.75 0)-
baseline 6
iterations 5

# Koch-unknown-angle
points -(0.33333333333333 0)-(0.5 0.55277079839256664151)-(0.6666666666666667 0)-
baseline 2
iterations 10

# Tartan
points -(0.4 0)-(0.5 -0.1)-(0.5 -0.5)-(0.5 -0.1)-(0.6 0)-
baseline 4
iterations 8

# Clouds
points -(0.19333333 -0.19333333)-(0.5 -0.06)-(1.18 0.16666667)-
baseline 2
iterations 8

# Exotic
points -(0.28 -0.11333333)-(0.5 0.4)-(0.72 -0.11333333)-
baseline 3
iterations 9

# Spiky sea-horse
points -(0.30666667 0.2)-(0.5 -0)-(1.29333333 -0.16666667)-
baseline 2
iterations 20

# Blotchy
points -(0.50666667 0.38)-(0.55333333 -0.27333333)-
baseline 1
iterations 10

# Twisty
points -(0.1666666667 0)-(0.1666666667 0.3333333333)-(0.5 0.3333333333)-(0.500000 0.3333333333)-(0.5 0.1666666667)-(0.3333333333 0.1666666667)-(0.3333333333 0)-(0.6666666667 0)-(0.6666666667 -0.1666666667)-(0.5 -0.1666666667)-(0.5 -0.3333333333)-(0.8333333333 -0.3333333333)-(0.8333333333 0)-

# Peano triangle
points -(0.5 0)-(0.25 0.4330127018922)-(0.5 0)-
baseline 3
iterations 8

# Peano hexagon
points -(0.5 0)-(0.25 -0.4330127018922)-(0.75 0.4330127018922)-(0.5 0)-
baseline 1
iterations 8

# Peano square
points -(0.3333333333 0)-(0.3333333333 0.3333333333)-(0.6666666667 0.3333333333)-(0.6666666667 0)-(0.3333333333 0)-(0.3333333333 -0.3333333333)-(0.6666666667 -0.3333333333)-(0.6666666667 0)-
baseline 1
iterations 6

# Box fractal
points -(0.3333333333 0)-(0.3333333333 -0.3333333333)-(0.6666666667 -0.3333333333)-(0.6666666667 0)-
baseline 4
iterations 6

# Wheeled square
points -(0.3333333333 0)-(0.3333333333 0)-(0.3333333333 -0.3333333333)-(0.3333333333 0)-(0.6666666667 0)-(0.6666666667 -0.3333333333)-(0.6666666667 0)-
baseline 4
iterations 5

# Not fudgeflake
points -(0.33333333333333 0)-(0.5 0.2886751345948)-(0.8333333333333 0.2886751345948)-
baseline 3
iterations 6

# Anti-Koch-alike
points -(0.25 0)-(0.5 -0.2886751345948)-(0.75 0)-
baseline 3
iterations 6

# Another anti-Koch-alike
points -(0.2 0)-(0.5 -0.218223035)-(0.8 0)-
baseline 3
iterations 6

# Unnamed
points -(0.250000 0.000000)- (0.500000 -0.288670)- (0.250000 0.000000) (0.750000 0.000000)- (0.500000 -0.288670)- (0.750000 0.000000)-
baseline 4
iterations 2

# Unnamed 2
points -(0.250000 0.000000)- (0.500000 -0.288670)- (0.250000 0.000000)- (0.500000 0.288670)- (0.750000 0.000000)- (0.500000 -0.288670) (0.750000 0.000000)-
baseline 6
iterations 6

# Fractured
points -(-0.42 -0.366666666666667) (1.23333333333333 0.786666666666667)-
baseline 1
iterations 20

# Pentagonal spiral
points -(0.126666666666667 -0.713333333333333) (1.46 0.406666666666667)-
baseline 1
iterations 35

# Shaded tri-spiral
points -(0 0.486666666666667) (1.33277993982348 0.828554135887042)-
baseline 1
iterations 36

# Splodge
points -(0.5 0.2886751345948)-(0.5 -0.2886751345948)-(0.5 0.2886751345948)-
baseline 6
iterations 8

# Splodge 2
points -(0.5 0.2886751345948)-(0.5 -0.2886751345948) (0.5 0.2886751345948)-
baseline 3
iterations 10

# Fig 5.15
points -(0 0.7) (0.3 0)-
baseline 1
iterations 12

# Fig 5.16
points -(0.6 0.6) (0.47 0)-
baseline 1
iterations 16

# Fig 5.17
points -(0 0.707106781186548) (0.5 0.5)-
baseline 1
iterations 16

# Generated by Meander 0.0.H
points  (-0.2071 0.7071)-(0.146446 0.7071)-(0.5 0.7071)-(0.853553 0.7071) (0.1464 .353)-(0.5 .353)-(0.853553 .353)-(1.2071067 .353) (0.5 1.06)-(0.8535 1.06) (0.1464 0)-(0.5 0) 
baseline 1
iterations 2

# Stingray
points -(0.5 0)-(1 0) (0.25 -0.25)-(0.25 0.25)-(0.25 -0.25) 
baseline 1
iterations 8
