#!perl -w
#______________________________________________________________________
# Geometric operations
# [email protected], 2003.
#______________________________________________________________________

my $VERSION = 1.2;

=head1 NAME

Geops - draw geometric figures using compass and straight edge only.

=head1 DESCRIPTION

Geometric constructions using compass and straight-edge only.
The right mouse button draws circles.
The left mouse button draws straight lines.
The center mousewheel zooms in and out.
Control-Z to undo.
Control-R to redo.
Left Doubleclick for more options.

Try drawing:

One line parallel to another
Lines at 30, 60, 90 degrees to another
An Isoscelese triangle
An equilateral triangle
A square
A hexagon
A pentagon
A circle through three non colinear points

Try drawing diagrams that demonstrate:

The theorem of pythagoras
cos(a+b)
sin(a+b)
Shearing a triangle does not change its area
the diagonals of a rhombus meet at 90.
Angle doubling in a circle
Right triangle in semi-circle
Bisection of a circle

Given a triangle, draw a circle:
- through the triangle's vertices
- tangentially touching the sides of the triangle,
  with the center inside the triangle
- tangentially touching the sides of the triangle with
  the center of the circle outside the triangle, and
  two sides of the triangle extended into lines.

=head1 README

Draw geometric figures using compass and straight edge only.

=head1 PREREQUISITES

C<Tk>

=head1 COREQUISITES

=pod OSNAMES

any

=pod SCRIPT CATEGORIES

Educational

=cut
#______________________________________________________________________
# Packages
#______________________________________________________________________

use Tk;
use Tk::Balloon;

#______________________________________________________________________
# Line manipulation
# [email protected], Novosoft Inc., 2003
#______________________________________________________________________

package line;
use Carp;

#______________________________________________________________________
# Create a line
# A line is characterized by the two points through which it passes
#______________________________________________________________________

sub new($$$$)
{my $l = bless {};   # line

 my $sx = shift;     # X point 1
 my $sy = shift;     # Y point 1
 my $fx = shift;     # X point 2
 my $fy = shift;     # Y point 2

 my $dx = ($fx-$sx); # Delta X
 my $dy = ($fy-$sy); # Delta Y

 $l->{sx} = $sx;
 $l->{sy} = $sy;
 $l->{fx} = $fx;
 $l->{fy} = $fy;
 $l->{dx} = $dx;
 $l->{dy} = $dy;

 croak "Bad line defined" if $dx == 0  and $dy == 0;
 return $l;
}

#______________________________________________________________________
# Intersect with box - find the points where a line crosses a box
#______________________________________________________________________

sub intersectWithBox($$$$$)
{my $l   = shift; # line
 my $bx1 = shift; # Lower left  X of box
 my $by1 = shift; # Lower right Y of box
 my $bx2 = shift; # Lower left  X of box
 my $by2 = shift; # Lower right Y of box

 my ($sx, $sy, $fx, $fy, $dx, $dy) = @$l{qw(sx sy fx fy dx dy)};

 my ($i, @i);

#______________________________________________________________________
# Special cases
#______________________________________________________________________

# Points too close

 return undef if abs($dx) <= 1 and abs($dy) <= 1;

# Vertical line

 return ($sx, $by1, $sx, $by2) if abs($dx) <= 1;

# Horizontal line

 return ($bx1, $sy, $bx2, $sy) if abs($dy) <= 1;

#______________________________________________________________________
# Intersection with each line bounding the box
#______________________________________________________________________

# Lower

 $i = $sx-$dx*($sy-$by1)/$dy;
 push @i, ($i, $by1) if $i >= $bx1 and $i <= $bx2;

# Upper

 $i = $sx-$dx*($sy-$by2)/$dy;
 push @i, ($i, $by2) if $i >= $bx1 and $i <= $bx2;
 return @i if scalar(@i) == 4;

# Right

 $i = $sy-$dy*($sx-$bx2)/$dx;
 push @i, ($bx2, $i) if $i >= $by1 and $i <= $by2;
 return @i if scalar(@i) == 4;

# Left

 $i = $sy-$dy*($sx-$bx1)/$dx;
 push @i, ($bx1, $i) if $i >= $by1 and $i <= $by2;

 return @i;
}

#______________________________________________________________________
# Determinant
#______________________________________________________________________

sub determinant($$$$)
{my ($x1, $y1, $x2, $y2) = @_;
 return ($x1*$y2 - $x2*$y1);
}

#______________________________________________________________________
# Intersection of two lines
#______________________________________________________________________

sub intersection(@)
{my ($p10, $p11, $p20, $p21, $p30, $p31, $p40, $p41) = @_;
 my $n = determinant($p30-$p10, $p30-$p40, $p31-$p11, $p31-$p41);
 my $d = determinant($p20-$p10, $p30-$p40, $p21-$p11, $p31-$p41);

 return undef if abs($d) < 1;

 return ($p10 + $n/$d * ($p20 - $p10),
         $p11 + $n/$d * ($p21 - $p11));
}

#______________________________________________________________________
# Point on a line closest to a point
# P1, P2 line, P3 point
#______________________________________________________________________

sub pointOnLineClosestToPoint(@)
{my ($p10, $p11, $p20, $p21, $p30, $p31) = @_;

 my $p40 = $p30 + $p21 - $p11; # Second point of line through P3
 my $p41 = $p31 - $p20 + $p10; # at right angles to line through P1, P2

 return intersection($p10, $p11, $p20, $p21, $p30, $p31, $p40, $p41);
}

#______________________________________________________________________
# Unit vector along a line
#______________________________________________________________________

sub unitVectorAlongLine(@)
{my ($p10, $p11, $p20, $p21) = @_;

 my ($x, $y) = (($p10-$p20), ($p11-$p21));
 return undef if $x == 0 and $y == 0;

 my $d = sqrt($x*$x+$y*$y);
 return ($x/$d, $y/$d);
}

#______________________________________________________________________
# Package loaded successfully
#______________________________________________________________________

1;

#______________________________________________________________________
# Display a dialog for selection of line thickness and dash pattern
# [email protected], 2003.
#______________________________________________________________________

package lineStyle;

sub new($@)
{my $m = shift;  # Main Window
 my %p = (-selected=>'green', -unselected=>'white', -flash=>'red', -entered=>'pink', -background=>'white',
          -line=>'blue', -widths=>[1..5], -dash=>['', qw(. - -. -..)], -height=>20, -width=>40, @_);
 my @w = (@{$p{'-widths'}});
 my @lineDraw = (5, $p{'-height'}/2+2, $p{'-width'}-2, $p{'-height'}/2+2);
 my @dash  = @{$p{'-dash'}};
 my $dash  = 1;
 my $width = 1;
 my @cdash = ();
 my @cline = ();
 my $row   = 1;
 my $n = scalar(@w); $n = scalar(@dash) if scalar(@dash) > $n;

 my $dw = $m->LabFrame(-label=>'Line types', -labelside=>'acrosstop')->pack();
 my $l1 = $dw->Label(-text=>'Width')->grid(-column=>1, -row=>$row);
 my $l2 = $dw->Label(-text=>'Style')->grid(-column=>2, -row=>$row);
 ++$row;

# Line width

 for(my $i = 0; $i < $n; ++$i)
  {if (defined($w[$i]))
    {my $c;
     my $enter = sub($$)
      {my $c = shift;
       my $i = shift;
       $c->configure(-background=>$p{'-entered'}) unless $i == $width;
      };

     my $leave = sub($$)
      {my $c = shift;
       my $i = shift;
       $c->configure(-background=>$p{'-unselected'}) unless $i == $width;
      };

     my $press = sub($$)
      {my $c = shift;
       my $i = shift;
       $c->configure(-background=>$p{'-flash'});
      };

     my $release = sub($$)
      {my $c = shift;
       my $i = shift;
       $width = $i;
       for(my $j = 0; $j < $n; ++$j)
        {$cline[$j]->configure(-background=>$p{'-unselected'});
        }
       $c->configure(-background=>$p{'-selected'});
       ${$p{'-widthVar'}} = $w[$width] if defined $p{'-widthVar'};
      };

     $cline[$i] = $c = $dw->Canvas(-height=>$p{'-height'}, -width=>$p{'-width'},
       -background=>$p{'-background'})->grid(-column=>1, -row=>$row);
     $c->configure(-background=>$p{'-selected'}) if defined($p{'-widthVar'}) and $w[$i] == ${$p{'-widthVar'}};
     $c->createLine(@lineDraw, -fill=>$p{'-line'}, -width=>$w[$i]);
     $c->CanvasBind("<ButtonRelease-1>", [$release, $i]);
     $c->CanvasBind("<ButtonPress-1>",   [$press,   $i]);
     $c->CanvasBind("<Enter>",           [$enter,   $i]);
     $c->CanvasBind("<Leave>",           [$leave,   $i]);
    }

# Line dash style

   my $d = $dash[$i];
   if (defined($d))
    {my $c;
     my $enter = sub($$)
      {my $c = shift;
       my $i = shift;
       $c->configure(-background=>$p{'-entered'}) unless $i == $dash;
      };

     my $leave = sub($$)
      {my $c = shift;
       my $i = shift;
       unless($i == $dash)
        {$c->configure(-background=>$p{'-unselected'});
        }
      };

     my $press = sub($$)
      {my $c = shift;
       my $i = shift;
       $c->configure(-background=>$p{'-flash'});
      };

     my $release = sub($$)
      {my $c = shift;
       my $i = shift;
       $dash = $i;
       for(my $j = 0; $j < $n; ++$j)
        {$cdash[$j]->configure(-background=>$p{'-unselected'});
        }
       $c->configure(-background=>$p{'-selected'});
       ${$p{'-dashVar'}} = $dash[$dash] if defined $p{'-dashVar'};
      };

     $cdash[$i] = $c = $dw->Canvas(-height=>$p{'-height'}, -width=>$p{'-width'},
       -background=>$p{'-background'})->grid(-column=>2, -row=>$row);
     $c->configure(-background=>$p{'-selected'}) if defined($p{'-dashVar'}) and $dash[$i] eq ${$p{'-dashVar'}};
     $c->createLine(@lineDraw, -fill=>$p{'-line'}, -dash=>$dash[$i], -width=>$i);
     $c->CanvasBind("<ButtonRelease-1>", [$release, $i]);
     $c->CanvasBind("<ButtonPress-1>",   [$press,   $i]);
     $c->CanvasBind("<Enter>",           [$enter,   $i]);
     $c->CanvasBind("<Leave>",           [$leave,   $i]);
    }
   ++$row;
  }
 return $dw;
}

#______________________________________________________________________
# Package loaded successfully
#______________________________________________________________________

1;

#______________________________________________________________________
# Get/Set
# [email protected], 2003.
#______________________________________________________________________

package gs;
use Carp;
#use Strict;

sub new()
{return bless {};
}

#______________________________________________________________________
# Get - retrieve values of global importance
#______________________________________________________________________

sub get($@)
{my $g = shift;
 my @p = @_;
 return $g->{$p[0]}                     if scalar(@p) == 1;
 return $g->{$p[0]}->{$p[1]}            if scalar(@p) == 2;
 return $g->{$p[0]}->{$p[1]}->{$p[2]}   if scalar(@p) == 3;
 die "geo::get: Wrong number of parameters";
}

#______________________________________________________________________
# Set - record values of global importance
#______________________________________________________________________

sub set($@)
{my $g = shift;
 my @p = @_;
 return $g->{$p[0]} = $p[1]                    if scalar(@p) == 2;
 return $g->{$p[0]}->{$p[1]} = $p[2]           if scalar(@p) == 3;
 return $g->{$p[0]}->{$p[1]}->{$p[2]} = $p[3]  if scalar(@p) == 4;
 die "geo::set: Wrong number of parameters";
}

#______________________________________________________________________
# Main
#______________________________________________________________________

package main;

print << 'END';

GEOPS: [email protected], 2003-2004

Geometric constructions using compass and straight-edge only.
The right mouse button draws circles.
The left mouse button draws straight lines.
The center mousewheel zooms in and out.
Control-Z to undo.
Control-R to redo.
Left Doubleclick for more options.

Try drawing:

One line parallel to another
Lines at 30, 60, 90 degrees to another
An Isoscelese triangle
An equilateral triangle
A square
A hexagon
A pentagon
A circle through three non colinear points

Try drawing diagrams that demonstrate:

The theorem of pythagoras
cos(a+b)
sin(a+b)
Shearing a triangle does not change its area
the diagonals of a rhombus meet at 90.
Angle doubling in a circle
Right triangle in semi-circle
Bisection of a circle
Given a triangle, draw a circle:
- through the triangle's vertices
- tangentially touching the sides of the triangle,
  with the center inside the triangle
- tangentially touching the sides of the triangle with
  the center of the circle outside the triangle, and
  two sides of the triangle extended into lines.
END


#______________________________________________________________________
# Get X, Y coords of mouse.  Round to nearest object if we are close
#______________________________________________________________________

sub getXYFromEvent($)
{my $w = shift;
 my $e = $w->XEvent;
 my ($x, $y) = areWeNearAnything(($c->canvasx($e->x), $c->canvasy($e->y)));
 return ($x, $y, $e->b);
}

#______________________________________________________________________
# Button press - record mouse position and start new object
#______________________________________________________________________

sub buttonPress($)
{($bx, $by) = getXYFromEvent(shift());

 $c->createOval($bx-$ps, $by-$ps, $bx+$ps, $by+$ps, -tags=>'startPoint', -fill=>'red');

# Undo / redo capability

 if (defined($objoff) and $objoff < scalar(@obj))
  {my @d = splice @obj, $objoff;
   for my $o(@d)
    {$c->delete($o->{tag}) if defined $o->{tag};
    }
   $objoff = undef;
  }
}

#______________________________________________________________________
# Button release - finish new object unless back where we started
#______________________________________________________________________

sub buttonRelease($)
{my ($x, $y, $b) = getXYFromEvent(shift());

 $c->delete('startPoint');

# Finish drawing line

 if ($b == 1)
  {$c->delete('currentLine');
   my $h = abs($y-$by) < $pc; $y = $by if $h;
   my $v = abs($x-$bx) < $pc; $x = $bx if $v;

  unless (($x-$bx)**2+($y-$by)**2 < $ps*$ps)
    {my $t = $c->createLine($bx, $by, $x, $y, -tags=>[$drawColor, 'line'],
     -fill =>$drawColor, -activefill =>'blue',        -disabledfill =>'yellow',
     -width=>$drawWidth, -activewidth=>$drawWidth+1,  -disabledwidth=>$drawWidth,
     -dash =>$drawDash);

     my $o = {type=>'line', vertical=>$v, horizontal=>$h, tag=>$t};
     push @obj, ({type=>'commit'}, $o);
     findIntersections($o);
    }
  }

# Finish drawing circle

 elsif ($b == 3)
  {$c->delete('currentCircle');

   my $r = sqrt(($x-$bx)**2+($y-$by)**2);
   unless ($r < $ps)
    {my $t1 = $c->createOval($bx-$r, $by-$r, $bx+$r, $by+$r, -tags=>[$drawColor, 'circle'],
     -outline=>$drawColor, -activeoutline=>'blue',     -disabledoutline=>'yellow',
     -width  =>$drawWidth, -activewidth=>$drawWidth+1, -disabledwidth=>$drawWidth,
     -dash   =>$drawDash);

     my $t2 = drawPoint($bx, $by, ["circleCenter$t1"]);

     my $o1 = {type=>'circle', tag=>$t1};
     my $o2 = {%$t2, centerOfCircle=>$t1};
     push @obj, ({type=>'commit'}, $o1, $o2);
     findIntersections($o1);
    }
  }
 $c->raise('point');
}

#______________________________________________________________________
# Button 1 motion - draw line
#______________________________________________________________________

sub button1Motion($)
{my ($x, $y) = getXYFromEvent(shift());

 return if configureStartPoint($x, $y);

 my $h = abs($y-$by) < $pc; $y = $by if $h;
 my $v = abs($x-$bx) < $pc; $x = $bx if $v;

 $c->delete('currentLine');

 my @i   = ($bx, $by, $x, $y);
 $c->createLine(@i, -width=>$drawWidth, -tags=>'currentLine', -fill =>'blue', -width=>$drawWidth+1);
}

#______________________________________________________________________
# Button 2 motion - pan
#______________________________________________________________________

sub button2Motion($)
{my ($x, $y) = getXYFromEvent(shift());
 $c->move('all', $x-$bx, $y-$by);
 $c->move('startPoint', $bx-$x, $by-$y);
 ($bx, $by) = ($x, $y);
}

#______________________________________________________________________
# Button 3 motion - draw circle
#______________________________________________________________________

sub button3Motion($)
{my ($x, $y) = getXYFromEvent(shift());

 return if configureStartPoint($x, $y);

 my $r = sqrt(($x-$bx)**2+($y-$by)**2);

 $c->delete('currentCircle');

 $c->createOval($bx-$r, $by-$r, $bx+$r, $by+$r, -tags=>'currentCircle',
   -outline=>'blue', -width  =>$drawWidth+1);
}

#______________________________________________________________________
# Zoom in or out on mouse wheel
#______________________________________________________________________

sub mouseWheel($)
{my $e = shift;
 my $w = $e->XEvent;
 my ($x, $y, $d) = ($w->x, $w->y, $w->D);
 my ($cx, $cy)   = ($c->canvasx($x), $c->canvasy($y));

 my ($xv1, $xv2) = $c->xview;
 my ($yv1, $yv2) = $c->yview;

 if ($d > 0)   # Zoom out
  {$c->scale('all', $x, $y, 4/5, 4/5);
   my $fx = $xv1 - $cx/5/4/$g->get(qw(display x)); $fx = 0 if $fx < 0;
   my $fy = $yv1 - $cy/5/4/$g->get(qw(display y)); $fy = 0 if $fy < 0;
   $c->xviewMoveto($fx);
   $c->yviewMoveto($fy);
  }
 else          # Zoom in
  {$c->scale('all', $x, $y, 5/4, 5/4);
   my $fx = $xv1 + $cx/$g->get(qw(display x)); $fx = 1 if $fx > 1;
   my $fy = $yv1 + $cy/$g->get(qw(display y)); $fy = 1 if $fy > 1;
   $c->xviewMoveto($fx);
   $c->yviewMoveto($fy);
  }

 redrawAllPoints();
}

#______________________________________________________________________
# Double Click - show actions dialog
#______________________________________________________________________

sub doubleButtonPress1Point($$)
{my $c   = shift();                 # Canvas press took place on
 my $t   = shift();                 # Tag of point selected
 my $lastTag  = $t;                 # Last tag selected
 my $startTag = $t;                 # Starting tag
 my $cl  = $c->itemcget($t, -fill=>);
 my $row = 0;                       # Grid row for next button
 my %ba  = qw(-anchor w -width 8);  # Default button attributes
 my %ga  = qw(-sticky w);           # Default button attributes
 my %cb  = ();                      # Hash of check buttons

# Dialog main window

 if (defined($mm))
  {$mm->raise();
   return;
  }

 $mm = MainWindow->new();
 $mm->title($g->get(qw(display title)));
 $mm->OnDestroy(sub {$mm = undef});

#______________________________________________________________________
# Color select
#______________________________________________________________________

 my $pm = $mm->LabFrame(-label=>'Color', -labelside=>'acrosstop')
  ->grid(-column=>1, -row=>1);
 $balloon->attach($pm, -msg=>"Choose the color you wish to draw in.\nYou can show or hide selected colors.");

 my $showInColor = sub ($)
  {my $r = shift; # Color that changed state

   my @t = $c->find(withtag=>'all');
   for my $t(@t)
    {my $l = colorFromTag($t);
     my $s = 'hidden';
        $s = 'normal' if $showColor->{$r} == 1;
     $c->itemconfigure($t, -state=>$s) if $l eq $r;
    }
  };

 my $changeColors = sub ()
  {my @t = $c->find(withtag=>'QED');
   for my $t(@t)
    {my $type = $c->type($t);
     $c->itemconfigure($t, -fill   =>$drawColor) if $type eq 'line';
     $c->itemconfigure($t, -outline=>$drawColor) if $type eq 'oval';
     $showColor->{$drawColor} = 1;
     &$showInColor($drawColor);
    }
   $cb{$drawColor}->select();
  };

 my $showColors = sub($)
  {my $l = shift; # Color that changed state
   &$showInColor($l);
  };

 my $t1 = $pm->Label(-text=>'Draw', -anchor=>'w')->grid(-column=>1, -row=>++$row, -sticky=>'w');
 my $t2 = $pm->Label(-text=>'Show', -anchor=>'e')->grid(-column=>2, -row=>  $row);

 for my $color(@drawColor)
  {my $bcolor = $color; $bcolor = 'white' if $color eq 'black';
   my $rb = $pm->Radiobutton(
      -text       => $color,
      -background => $bcolor,
      -selectcolor=> $bcolor,
      -variable   => \$drawColor,
      -value      => $color,
      -anchor     => 'w',
      -command    => $changeColors,
      )->grid(-column=>1, -row=>++$row, -sticky=>'we');

   my $cb = $pm->Checkbutton(
#     -text       => $color,
      -background => $bcolor,
      -selectcolor=> $bcolor,
      -variable   => \$showColor->{$color},
      -anchor     => 'center',
      -command    => [$showColors, $color],
      )->grid(-column=>2, -row=>$row, -sticky=>'we');

   $cb{$color} = $cb;
   $balloon->attach($rb, -msg=>"Draw in $color.");
   $balloon->attach($cb, -msg=>"Show or hide $color.");
  }

#______________________________________________________________________
# Line style select
#______________________________________________________________________

 my $lm = $mm->lineStyle::new(-selected=>'green', -flash=>'red', -entered=>'pink', -unselected=>'white',
     -background=>'white', -line=>'blue', -widthVar=>\$drawWidth, -dashVar=>\$drawDash,
     -widths=>[1..5], -dash=>['', qw(. - -. -..)], -height=>20, -width=>50)
   ->grid(-column=>1, -row=>2);

#______________________________________________________________________
# Files
#______________________________________________________________________

 my $print = sub
  {my $f = $m->getSaveFile(-defaultextension=>'.jpg', #-filetypes=>['JPG files', ['.jpg']],
                            -title=>'Choose a file to write the image to');

   $c->itemconfigure('point', -state=>'hidden');
   $c->postscript(-file=>"zzz.ps");
   $c->itemconfigure('point', -state=>'normal');
   my $cmd = $gs;
   $cmd =~ s/XXX/$f/;
   `$cmd`;
   $m->messageBox(-message=>"Image written to $f", -title=>'Success!', -type=>'OK');
  };

 my $new  = sub {print "New not implemented yet\n"};
 my $save = sub {print "Save not implemented yet\n"};


 my $fm = $mm->LabFrame(-label=>'Files', -labelside=>'acrosstop')->grid(-column=>1, -row=>3);
 my $pb = $fm->Button(-text=>'Print', -command=>$print)->grid(-column=>1, -row=>1);
 my $nb = $fm->Button(-text=>'New',   -command=>$new)  ->grid(-column=>2, -row=>1);
 my $sb = $fm->Button(-text=>'Save',  -command=>$save) ->grid(-column=>3, -row=>1);

 $balloon->attach($pb, -msg=>"Create JPEG");
 $balloon->attach($nb, -msg=>"New file to contain data");
 $balloon->attach($sb, -msg=>"Save data to file");
}

#______________________________________________________________________
# Are we near anything - check how close a point is to known objects
# This could be improved by using $c->bbox
#______________________________________________________________________

sub areWeNearAnything($$)
{my $x = shift; # X position
 my $y = shift; # Y position
 my $n = $pc;

 for my $o(@obj)
  {if ($o->{type} eq 'point' and !defined($o->{reuse}))
    {my ($cx, $cy) = coordsOfPoint($o->{tag});
     my $d = ($x-$cx)**2+($y-$cy)**2; # Squared distance to center
     return ($cx, $cy) if $d < $n*$n; # Substitute center of circle
    }
  }
 return ($x, $y);
}

#______________________________________________________________________
# findIntersections - last object added with existing objects
#______________________________________________________________________

sub findIntersections($)
{return unless scalar(@obj) > 0;   # No intersections yet

 my $a = shift;
  {my %a = %$a;
   next unless $a{type} eq 'line' or $a{type} eq 'circle';

   for my $o(@obj)
    {my %o = %$o;
     next unless $o{type} eq 'line' or $o{type} eq 'circle';
     next unless colorFromTag($o{tag}) eq colorFromTag($a{tag});

#______________________________________________________________________
# Intersect circle and circle
# r,R: Radii of circles.
# D:   Distance between centers.
# d:   Half of major axis of chord of intersection
# e:   Distance to chord from one center
# T:   Angle of line drawn through centers to horizontal.
# t:   Half angle subtended by 'd' from center of one circle
# sin(a+b) = sin(a)cos(b)+cos(a)sin(b)
# sin(a-b) = sin(a)cos(b)-cos(a)sin(b)
# cos(a+b) = cos(a)cos(b)-sin(a)sin(b)
# cos(a-b) = cos(a)cos(b)+sin(a)sin(b)
#______________________________________________________________________

     if ($a{type} eq 'circle' and $o{type} eq 'circle')
      {my $r         = radiusOfCircle($a);
       my $R         = radiusOfCircle($o);
       my ($cx, $cy) = centerOfCircle($a);
       my ($Cx, $Cy) = centerOfCircle($o);
       my $D = sqrt(($cx-$Cx)**2+($cy-$Cy)**2); # Distance between two centers
       next if $D > $R+$r;                      # Too far apart to intersect
       next if $D < $ps;                        # Too close to intersect

       my $dd = $R*$R - ($R*$R-$r*$r+$D*$D)**2/(4*$D*$D); # Half chord width squared
       my $d = sqrt(abs($dd));                  # Half chord width
       my $e = sqrt($r*$r - $dd);               # Distance to half chord from center of circle
       my $cosT = ($Cx-$cx) / $D;               # cos(T)
       my $sinT = ($Cy-$cy) / $D;               # sin(T)
       my $sint = $d/$r;
       my $cost = $e/$r;

       my $sinTpt = $sinT*$cost+$cosT*$sint;
       my $cosTpt = $cosT*$cost-$sinT*$sint;
       my $sinTmt = $sinT*$cost-$cosT*$sint;
       my $cosTmt = $cosT*$cost+$sinT*$sint;

       my @i = ([$cx+$cosTpt*$r, $cy+$sinTpt*$r],
                [$cx+$cosTmt*$r, $cy+$sinTmt*$r]);

       for my $i(@i)
        {my ($x, $y) = @$i;
         my $t = drawPoint($x, $y, ["intersectCircle$a{tag}Circle$o{tag}"]);
         push @obj, {%$t, intersectCircles=>[$a, $o]};
        }
      }

#______________________________________________________________________
# Intersect line and line
#______________________________________________________________________

     if ($a{type} eq 'line' and $o{type} eq 'line')
      {my @a = coordsOfLine($a);
       my @o = coordsOfLine($o);
       my ($x, $y) = line::intersection(@a, @o);
       next unless defined $x;

       my $t = drawPoint($x, $y, ["intersectLine$a{tag}Line$o{tag}"]);
       push @obj, {%$t, intersectLines=>[$a, $o]};
      }

#______________________________________________________________________
# Intersect line and circle
# Find the point on the line closest to the center of the circle.
# This point is midway between the two intersection points.
#______________________________________________________________________

     if (($a{type} eq 'line' and $o{type} eq 'circle') or
         ($o{type} eq 'line' and $a{type} eq 'circle'))
      {my %l = %a; %l = %o if $o{type} eq 'line';
       my %c = %o; %c = %a if $a{type} eq 'circle';

       my @l = coordsOfLine(\%l);
       my @c = centerOfCircle(\%c);
       my $r = radiusOfCircle(\%c);
       my ($X, $Y) = line::pointOnLineClosestToPoint(@l, @c);
       next unless defined $X;

       my $dd = ($c[0]-$X)**2+($c[1]-$Y)**2;  # Distance squared from midway to center
       next if sqrt($dd) > $r;                # Check actually intersects circle
       my $d  = sqrt($r**2-$dd);              # Distance from midway to circumference

       my ($ux, $uy) = line::unitVectorAlongLine(@l);

       my $t1 = drawPoint($X + $d * $ux, $Y + $d * $uy, ["intersectLine$l{tag}Circle$c{tag}"]);
       push @obj, {%$t1, intersectLineCircle=>[\%l, \%c]};

       my $t2 = drawPoint($X - $d * $ux, $Y - $d * $uy, ["intersectLine$l{tag}Circle$c{tag}"]);
       push @obj, {%$t2, intersectLineCircle=>[\%l, \%c]};
      }
    }
  }
}

#______________________________________________________________________
# Redraw all points to correct size
#______________________________________________________________________

sub redrawAllPoints()
{for my $p(@obj)
  {my %p = %$p;
   next unless $p{type} eq 'point' and !defined $p{reuse};
   my @i       = $c->coords($p{tag});
   my ($x, $y) = coordsOfPoint($p{tag});
   $c->coords($p{tag}, $x-$ps, $y-$ps, $x+$ps, $y+$ps);
  }
}

#______________________________________________________________________
# Draw point unless very close to an existing point
#______________________________________________________________________

sub drawPoint($$)
{my $x  = shift; # X coord
 my $y  = shift; # Y coord
 my $t  = shift; # Array of tags

 my @n = $c->find(overlapping=>$x-$ps, $y-$ps, $x+$ps, $y+$ps);

 for my $n(@n)
  {my @t = $c->gettags($n);
   my %t;
   for my $t(@t) {$t{$t} = 1};
   if ($t{point} and $t{$drawColor})
    {my ($cx, $cy) = coordsOfPoint($n);
     my $d = ($cx-$x)**2+($cy-$y)**2;
     return {type=>'point', reuse=>$n} if $d < $near;
    }
  }

 my $p = $c->createOval($x-$ps, $y-$ps, $x+$ps, $y+$ps, -tags=>['point', $drawColor, @$t],
   -outline=>$drawColor, -fill=>'white', -activefill=>'green', -disabledfill=>'yellow');
 $c->bind($p, "<Double-ButtonPress-1>", [\&doubleButtonPress1Point, $p]);
 return {type=>'point', tag=>$p};
}

#______________________________________________________________________
# Configure start point
#______________________________________________________________________

sub configureStartPoint($$)
{my ($x, $y) = @_;

 if (($x-$bx)**2+($y-$by)**2 < $ps*$ps)
  {$c->itemconfigure('startPoint', -fill=>'red');
   return 1;
  }
 else
  {$c  ->itemconfigure('startPoint', -fill=>'green');
   return 0;
  }
}

#______________________________________________________________________
# Coords of line from tag
#______________________________________________________________________

sub coordsOfLine($)
{my $l = shift; # Line
 return $c->coords($l->{tag});
}

#______________________________________________________________________
# Radius of circle from tag
#______________________________________________________________________

sub radiusOfCircle($)
{my $C = shift; # Circle
 my ($x1, $y1, $x2, $y2) = $c->coords($C->{tag});
 return abs($x2 - $x1) / 2;
}

#______________________________________________________________________
# Center of circle from tag
#______________________________________________________________________

sub centerOfCircle($)
{my $C = shift; # Circle
 my ($x1, $y1, $x2, $y2) = $c->coords($C->{tag});
 return (($x1+$x2)/2, ($y1+$y2)/2);
}

#______________________________________________________________________
# Coord of point from tag
#______________________________________________________________________

sub coordsOfPoint($)
{my $p = shift; # Tag of point
 my ($x1, $y1, $x2, $y2) = $c->coords($p);
unless ($x1)
{print "p=$p\n";
 dd();
}
 return (($x1+$x2)/2, ($y1+$y2)/2);
}

#______________________________________________________________________
# Color of object from tag
#______________________________________________________________________

sub colorFromTag($)
{my $t    = shift; # Tag
 my $type = $c->type($t);
 my $cl;
    $cl = $c->itemcget($t, -fill=>)    if $type eq 'line';
    $cl = $c->itemcget($t, -outline=>) if $type eq 'oval';
 return $cl;
}

#______________________________________________________________________
# Dump all objects
#______________________________________________________________________

sub dd($)
{my $l = shift; # Title
 print "\n";
 print "$l\n" if $l;
 my @t = $c->find(withtag=>'all');
 for my $t(@t)
  {my @v = $c->gettags($t);
   if (@v)
    {my @co = $c->coords($t);
      print "$t:", join(' ', @v), "\n  coords:", join(' ', @co), "\n";
    }
  }
}

#______________________________________________________________________
# Undo
#______________________________________________________________________

sub undo()
{$objoff = scalar(@obj) unless defined($objoff);
 $objoff-- if $objoff > 0;

 for(;$objoff >= 0; --$objoff)
  {return if $objoff < 0;
   my %o = %{$obj[$objoff]};
   my $t = ''; $t = $o{tag} if defined($o{tag});
   if ($o{type} eq 'commit')
    {return;
    }
   $c->itemconfigure($o{tag}, -state=>'disabled');
  }
}

#______________________________________________________________________
# Redo
#______________________________________________________________________

sub redo()
{return unless defined($objoff);
 $objoff++ if $objoff < scalar(@obj);

 for(;$objoff < scalar(@obj);++$objoff)
  {my %o = %{$obj[$objoff]};
   my $t = ''; $t = $o{tag} if defined($o{tag});
   if ($o{type} eq 'commit')
    {return;
    }
   $c->itemconfigure($o{tag}, -state=>'normal');
  }
}

#______________________________________________________________________
# Main
#______________________________________________________________________

$g = gs::new();
$g->set qw(display title Geops);    # X size of display
$g->set qw(display x 1000);         # X size of display
$g->set qw(display y 1000);         # Y size of display
$g->set qw(display near    0.001);  # Near enough to be considered the same
$g->set qw(user point size 5);      # Point representation size
$g->set qw(user point capture 10);  # Point representation size

#______________________________________________________________________
# Create display
#______________________________________________________________________

$m = MainWindow->new();
$m->title($g->get(qw(display title)));
$g->set(qw(display main), $m);

$m->OnDestroy(sub {$mm->destroy() if defined($mm)});

$c = $m->Canvas(
   -background => 'white',
   -width      => $g->get(qw(display x)),
   -height     => $g->get(qw(display y)),
   -cursor=>'crosshair');

$g->set(qw(display canvas),      $c);

$c->pack(-expand=>1, -fill=>'both');

$balloon = $m->Balloon(); # Help balloon

#______________________________________________________________________
# Data
#______________________________________________________________________

$ps   = $g->get qw(user point size);        # Point size
$pc   = $g->get qw(user point capture);     # Point capture size
$near = $g->get qw(display near);           # Near enough to be the same
$bx   = undef;                              # Button down X
$by   = undef;                              # Button down Y
@obj  = ();                                 # List of objects
@drawColor = qw/DarkRed Red DeepPink Magenta OrangeRed Orange Gold Yellow Cyan Green DarkGreen Purple Blue DarkBlue Black/;
$drawColor = 'Black';                       # Current color
$drawWidth = 3;                             # Current drawing width
$drawDash  = '';                            # Dash scheme
$showColor->{$drawColor} = 1;               # Activate current color

$gs = '/gs/"gs8.11"/bin/gswin32c.exe -sDEVICE=jpeg -SOutputFile=XXX -dBATCH -dNOPAUSE zzz.ps';

#______________________________________________________________________
# Bindings
#______________________________________________________________________

$c->CanvasBind("<ButtonPress>",                \&buttonPress);
$c->CanvasBind("<ButtonRelease>",              \&buttonRelease);
$c->CanvasBind("<Button1-Motion>",             \&button1Motion);
$c->CanvasBind("<Button2-Motion>",             \&button2Motion);
$c->CanvasBind("<Button3-Motion>",             \&button3Motion);
$c->CanvasBind('all', "<MouseWheel>",          \&mouseWheel);
$m->bind("<Control-z>",            \&undo);
$m->bind("<Control-r>",            \&redo);
$m->bind("<Double-ButtonPress-1>", \&doubleButtonPress1Point);

#______________________________________________________________________
# Display
#______________________________________________________________________

MainLoop;