Mathematical notebook revisited

Arjen Markus (19 december 2011) I am fascinated by mathematics in general and geometry in particular. So, this weekend I extended my old mathematical notebook with the specific purpose of visualising classical geometrical constructions. For this I needed an extended version - which you will find below (some convenience procedures and a different user-interface).

The idea is to use the flexibility of the text widget to allow for an interactive display of the construction: via tags the example below highlights the steps in the construction of lines tangent to a circles. All in the classical tradition of the ancient Greek.

I use the tag mechanism of the canvas to delete those items I do not need anymore - right now: just used by the "Reset" button.

(Note: it is not perfect yet - opening a second file for instance does not refresh the text widget properly)

Example: Tangent lines to a circle

Here is a screenshot:

Matematical notebook - screenshot

# Classical construction:
#     Tangent lines to a circle
#
<h1>Using compasses and straightedge</h1>
<p>
The classical tools for geometrical constructions, such as the hexagon
or the bissectrix of an angle are the compasses and the straightedge.
<p>
Here we show how to use these tools to construct the lines through a
given point that are tangent to a circle. The construction is shown in
steps.
(This illustrates that it is easy to make interactive displays).
<p>
First we draw the point P and the circle C (see the canvas on the
right-hand side)
<p>
Then we take the following steps (press the Next button):
<ul>
<tag>step1
<li>
Draw a line piece through the centre of the circle and the point.
<tag>step2
<li>
Construct the line through the middle of this line piece, perpendicular
to that line.
<tag>step3
<li>
The midpoint is the centre of a circle through the centre of C and P.
<tag>step4
<li>
Finally the points where the two circles intersect are the points where
the two tangents lines meet circle C.
<tag>step5
<li>
Draw the tangent lines.
</ul>
<tag>normal
This way we have constructed the tangent lines.

@init {
    proc resetTags {} {
        variable TXT
        variable fontNormal
        $TXT tag configure step1 -foreground lightgrey -font $fontNormal
        $TXT tag configure step2 -foreground lightgrey -font $fontNormal
        $TXT tag configure step3 -foreground lightgrey -font $fontNormal
        $TXT tag configure step4 -foreground lightgrey -font $fontNormal
        $TXT tag configure step5 -foreground lightgrey -font $fontNormal
    }
    resetTags

    proc showStep {step} {
        variable CNV
        variable xcentre
        variable ycentre
        variable radius
        variable xpoint
        variable ypoint
        variable xp      ;# Midpoint between point P and centre of circle C
        variable yp
        variable radiusM ;# Radius of the circle through P and centre
        variable tangentPoints

        switch $step {
            "0" {
                $CNV delete steps
            }
            "1" {
                #
                # Step: line piece connecting point and centre
                #
                set id [polyline [list $xcentre $ycentre $xpoint $ypoint]]
                $CNV itemconfigure $id -tag steps
            }
            "2" {
                #
                # Midpoint of the line piece
                #
                set id1 [circle $xcentre $ycentre 1.5]
                set id2 [circle $xpoint  $ypoint  1.5]
                $CNV itemconfigure $id1 -tag steps
                $CNV itemconfigure $id2 -tag steps
                #
                # Compute the two intersection points
                # and draw them, as well as the line piece between them
                #
                set points [circleIntersection [list $xcentre $ycentre 1.5] \
                                               [list $xpoint  $ypoint  1.5]]

                set id [polyline $points]
                $CNV itemconfigure $id -tag steps

                set xp [expr {([lindex $points 0] + [lindex $points 2]) / 2.0}]
                set yp [expr {([lindex $points 1] + [lindex $points 3]) / 2.0}]

                set id1 [point [lindex $points 0] [lindex $points 1] black]
                set id2 [point [lindex $points 2] [lindex $points 3] black]
                set id3 [point $xp $yp red]

                $CNV itemconfigure $id1 -tag steps
                $CNV itemconfigure $id2 -tag steps
                $CNV itemconfigure $id3 -tag steps
            }
            "3" {
                #
                # Step: circle through centre and point
                #
                set radiusM [expr {hypot($xcentre-$xp,$ycentre-$yp)}]

                set id [circle $xp $yp $radiusM red]
                $CNV itemconfigure $id -tag steps
            }
            "4" {
                #
                # Step: intersection points of the two circles
                #
                set tangentPoints [circleIntersection [list $xcentre $ycentre $radius] \
                                                      [list $xp      $yp      $radiusM]]
                set id1 [point [lindex $tangentPoints 0] [lindex $tangentPoints 1] green]
                set id2 [point [lindex $tangentPoints 2] [lindex $tangentPoints 3] green]
                $CNV itemconfigure $id1 -tag steps
                $CNV itemconfigure $id2 -tag steps
            }
            "5" {
                #
                # Step: draw the tangent lines
                #
                set id1 [infiniteLine [lindex $tangentPoints 0] [lindex $tangentPoints 1] $xpoint $ypoint green]
                set id2 [infiniteLine [lindex $tangentPoints 2] [lindex $tangentPoints 3] $xpoint $ypoint green]
                $CNV itemconfigure $id1 -tag steps -width 2
                $CNV itemconfigure $id2 -tag steps -width 2
            }

        }
    }
}

@canvasright 400 400 {
    variable state
    variable xcentre
    variable ycentre
    variable xpoint
    variable ypoint
    variable radius
    variable CNV

    set state 0

    scale {-2.5 -2.0 1.5 2.0}

    set xcentre -1.0
    set ycentre  0.0
    set xpoint   0.7
    set ypoint   0.6
    set radius   0.8

    set id [circle $xcentre $ycentre $radius black] ;# Circle C
    $CNV itemconfigure $id -width 2
    point  $xpoint  $ypoint  black         ;# Point P

    set state 0
}
@button Reset {
    resetTags
    $CNV delete steps
    set state 0
}
@button Next {
    variable state

    resetTags

    incr state
    $TXT tag configure step$state -foreground black -font $fontBold
    showStep $state
}

Example: The witch of Agnesi

Here is a second example, as it presents an animated construction, you will have to run it yourself to see it.

# witch_agnesi.txt --
#     Construct the curve known as the witch of Agnesi
#

<h1>Witch of Agnesi</h1>
<p>
With compasses and straightedge you can construct all manner of curves,
though sometimes the process is more mechanical than mathematical.
<p>
Here is an example: the witch of Agnesi is constructed by drawing
a line through a fixed point on a circle and using the intersection
through that circle and a tangent line to define a new point.
<p>
The process is illustrated in the figure on the right. Press the
"Go" button to see how it works.
<p>
The curve has the parametric form:
<pre>
     x = 2 a cot t
     y = a (1-cos(2t))
</pre>
where "a" is the radius of the circle (cf. mathworld.wolfram.com/WitchofAgnesi.html).

@init {
    proc intersectionCircleLine {circle line} {
        foreach {xc yc radius} $circle {break}
        foreach {x1 y1 x2 y2}  $line   {break}

        set dx     [expr {$x2 - $x1}]
        set dy     [expr {$y2 - $y1}]
        set length [expr {hypot($dx,$dy)}]
        set xn     [expr {-$dy/$length}]
        set yn     [expr {$dx/$length}]

        set mu     [expr {($x1-$xc)*$xn + ($y1-$yc)*$yn}]

        set xmid   [expr {$xc + $mu * $xn}]
        set ymid   [expr {$yc + $mu * $yn}]

        set dist   [expr {sqrt($radius**2 - ($xmid-$xc)**2 - ($ymid-$yc)**2)/$length}]

        set xi1    [expr {$xmid + $dx * $dist}]
        set yi1    [expr {$ymid + $dy * $dist}]
        set xi2    [expr {$xmid - $dx * $dist}]
        set yi2    [expr {$ymid - $dy * $dist}]

        return [list $xi1 $yi1 $xi2 $yi2]
    }

    proc drawWitch {x} {
        variable CNV
        variable coords

        $CNV delete line
        $CNV delete witch
        set line [list 0.0 -1.0 $x 1.0]

        set id1 [polyline $line black]
        set id2 [point $x 1.0 red]

        $CNV itemconfigure $id1 -tag line
        $CNV itemconfigure $id2 -tag line

        #
        # Determine the points of intersection and select the
        # right one - the one with y > -1.0
        #
        set intersectionPoints [intersectionCircleLine {0.0 0.0 1.0} $line]

        foreach {xp yp} $intersectionPoints {
            if { $yp > -0.999 } {
                break
            }
        }
        #
        # Draw the auxiliary lines
        #
        set id1 [polyline [list $x  1.0 $x -1.0] red]
        set id2 [polyline [list $xp $yp $x  $yp] red]
        set id3 [point $xp $yp red]
        set id4 [point $x  $yp red]

        $CNV itemconfigure $id1 -tag line
        $CNV itemconfigure $id2 -tag line
        $CNV itemconfigure $id3 -tag line
        $CNV itemconfigure $id4 -tag line

        lappend coords $x $yp
        if { [llength $coords] > 2 } {
            set id [polyline $coords red]
            $CNV itemconfigure $id -tag witch
        }

        #
        # Note: the procedure is defined within the MathData namespace
        #
        if { $x < 4.0 } {
            after 100 [list ::MathData::drawWitch [expr {$x+0.1}]]
        } else {
            $CNV delete line
            $CNV itemconfigure $id -width 2
        }
    }
}

@canvasright 400 400 {
    scale {-4.0 -4.0 4.0 4.0}

    infiniteLine -2.0  1.0 2.0 1.0 black
    circle        0.0  0.0 1.0 black
    point         0.0 -1.0 black

    console show
    puts [intersectionCircleLine {0.0 0.0 1.0} {0.0 0.0 0.0 1.0}]
}

@button Go {
    variable coords

    set coords {}
    drawWitch -4.0
}

Code: Updated mathbook

# mathbook.tcl --
#    Script to show notes on mathematical subjects
#
# TODO:
#    - Implement a number of useful drawing commands
#    - Implement a formula renderer (a basic one _is_ available)
#    - Implement more convenient bindings
#    - Describe the application
#
#    Missing commands:
#    @refresh - define your own refresh method
#    @label   - allow a label (useful for variable text)
#    @button  - allow a pushbutton
#
package require Tcl 8.5
package require Tk

if { [tk windowingsystem] == "x11" } {
   . configure -background #dcdad5
   option add *background #dcdad5
   option add *foreground black
   option add *borderWidth 1 widgetDefault
   option add *activeBorderWidth 1 widgetDefault
   option add *selectBorderWidth 1 widgetDefault
   option add *font -adobe-helvetica-medium-r-normal-*-12-*-*-*-*-*-*

   option add *padX 2
   option add *padY 4

   option add *Listbox.background white
   option add *Listbox.selectBorderWidth 0
   option add *Listbox.selectForeground white
   option add *Listbox.selectBackground #4a6984

   option add *Entry.foreground black
   option add *Entry.background white
   option add *Entry.selectBorderWidth 0
   option add *Entry.selectForeground white
   option add *Entry.selectBackground #4a6984

   option add *Text.background white
   option add *Text.selectBorderWidth 0
   option add *Text.selectForeground white
   option add *Text.selectBackground #4a6984

   option add *Menu.activeBackground #4a6984
   option add *Menu.activeForeground white
   option add *Menu.activeBorderWidth 0
   option add *Menu.highlightThickness 0
   option add *Menu.borderWidth 2

   option add *MenuButton.activeBackground #4a6984
   option add *MenuButton.activeForeground white
   option add *MenuButton.activeBorderWidth 0
   option add *MenuButton.highlightThickness 0
   option add *MenuButton.borderWidth 0

   option add *highlightThickness 0
   option add *troughColor #bdb6ad
}


# MathData --
#    Namespace for the user-defined commands and data
#
namespace eval ::MathData:: {
   variable CNV          ""
   variable TXT          ""

   variable fontNormal   "Courier 10"
   variable fontBold     "Courier 10 bold"
   variable fontItalic   "Courier 10 italic"
}

# scale --
#    Set up the scaling for the given canvas
# Arguments:
#    data            List of data (x, y, x, y ...)
# Result:
#    None
# Side effects:
#    Scaling parameters set
# Note:
#    TODO: Should make sure there is some scaling involved
#          if only using pixels
#
proc ::MathData::scale { data } {
   variable CNV
   variable SCALE

   set width  [$CNV cget -width]
   set height [$CNV cget -height]
   set xmin  1.0e30
   set xmax -1.0e30
   set ymin  1.0e30
   set ymax -1.0e30

   foreach {x y} $data {
      if { $x < $xmin } { set xmin $x }
      if { $x > $xmax } { set xmax $x }
      if { $y < $ymin } { set ymin $y }
      if { $y > $ymax } { set ymax $y }
   }

   if { $xmin == $xmax } { set xmax [expr {$xmax+1.0}] }
   if { $ymin == $ymax } { set ymax [expr {$ymax+1.0}] }

   set SCALE(xscale) [expr {$width/double($xmax-$xmin)}]
   set SCALE(yscale) [expr {$height/double($ymax-$ymin)}]
   set SCALE(xmin)   $xmin
   set SCALE(xmax)   $xmax
   set SCALE(ymin)   $ymin
   set SCALE(ymax)   $ymax
}

# polyline --
#    Draw a line consisting of multiple points
# Arguments:
#    data            List of data (x, y, x, y ...)
#    colour          Colour to use (default: black)
# Result:
#    Canvas ID of the polyline
# Side effects:
#    Line drawn according to current scales
#
proc ::MathData::polyline { data {colour black} } {
   variable CNV
   variable SCALE

   set xscale $SCALE(xscale)
   set yscale $SCALE(yscale)
   set xmin   $SCALE(xmin)
   set xmax   $SCALE(xmax)
   set ymin   $SCALE(ymin)
   set ymax   $SCALE(ymax)

   set pixels {}
   foreach {x y} $data {
      set px [expr {$xscale*($x-$xmin)}]
      set py [expr {$yscale*($ymax-$y)}]
      lappend pixels $px $py
   }

   $CNV create line $pixels -fill $colour
}

# circle --
#    Draw a circle with given centre and radius
# Arguments:
#    xcentre         X-coordinate of the centre
#    ycentre         Y-coordinate of the centre
#    radius          Radius of circle
#    colour          Colour to use (default: black)
#    filled          Filled or not (default: not)
# Result:
#    Canvas ID of the circle
# Side effects:
#    Line drawn according to current scales
#
proc ::MathData::circle { xcentre ycentre radius {colour black} {filled 0} } {
   variable CNV
   variable SCALE

   set xscale $SCALE(xscale)
   set yscale $SCALE(yscale)
   set xmin   $SCALE(xmin)
   set xmax   $SCALE(xmax)
   set ymin   $SCALE(ymin)
   set ymax   $SCALE(ymax)

   set pixels {}
   foreach {x y} [list [expr {$xcentre-$radius}] [expr {$ycentre-$radius}] \
                       [expr {$xcentre+$radius}] [expr {$ycentre+$radius}] ] {
      set px [expr {$xscale*($x-$xmin)}]
      set py [expr {$yscale*($ymax-$y)}]
      lappend pixels $px $py
   }

   $CNV create oval $pixels -fill [expr {$filled? $colour : {}}] -outline $colour
}

# point --
#    Draw a point with given coordinates
# Arguments:
#    xpoint          X-coordinate
#    ypoint          Y-coordinate
#    colour          Colour to use (default: black)
# Result:
#    Canvas ID of the point
# Side effects:
#    Line drawn according to current scales
#
proc ::MathData::point { xpoint ypoint {colour black} } {
   variable CNV
   variable SCALE

   set xscale $SCALE(xscale)
   set yscale $SCALE(yscale)
   set xmin   $SCALE(xmin)
   set xmax   $SCALE(xmax)
   set ymin   $SCALE(ymin)
   set ymax   $SCALE(ymax)

   set pixels {}
   set px [expr {$xscale*($xpoint-$xmin)}]
   set py [expr {$yscale*($ymax-$ypoint)}]
   lappend pixels [expr {$px-1}] [expr {$py-1}] [expr {$px+1}] [expr {$py+1}]

   $CNV create rectangle $pixels -fill $colour -outline $colour
}

# text --
#    Draw a text string at a given position
# Arguments:
#    x               X coordinate
#    y               Y coordinate
#    string          String to show
# Result:
#    Canvas ID of the text object
# Side effects:
#    String drawn
#
proc ::MathData::text { x y string } {
   variable CNV
   variable SCALE

   set xscale $SCALE(xscale)
   set yscale $SCALE(yscale)
   set xmin   $SCALE(xmin)
   set xmax   $SCALE(xmax)
   set ymin   $SCALE(ymin)
   set ymax   $SCALE(ymax)

   set px [expr {$xscale*($x-$xmin)}]
   set py [expr {$yscale*($ymax-$y)}]

   $CNV create text $px $py -text $string -anchor nw
}

# axes --
#    Draw two lines representing the axes
# Arguments:
#    None
# Result:
#    None
# Side effects:
#    Two lines drawn (no labels yet)
#
proc ::MathData::axes { } {
   variable CNV
   variable SCALE

   set width  [$CNV cget -width]
   set height [$CNV cget -height]
   set xscale $SCALE(xscale)
   set yscale $SCALE(yscale)
   set xmin   $SCALE(xmin)
   set xmax   $SCALE(xmax)
   set ymin   $SCALE(ymin)
   set ymax   $SCALE(ymax)

   set px0 [expr {$xscale*(0.0-$xmin)}]
   set py0 [expr {$yscale*($ymax-0.0)}]

   $CNV create line $px0 0    $px0   $height -fill black
   $CNV create line 0    $py0 $width $py0    -fill black
}

# func --
#    Repeatedly run a function and return xy-pairs
# Arguments:
#    funcname       Name of the function (procedure)
#    xmin           Minimum x-value
#    xmax           Maximum x-value
#    nosteps        Number of steps (inbetween; default: 50)
# Result:
#    List of x, y values
#
proc ::MathData::func { funcname xmin xmax { nosteps 50 } } {

   set coords {}
   set xstep  [expr {($xmax-$xmin)/$nosteps}]

   for { set i 0 } { $i <= $nosteps } { incr i } {
      set x [expr {$xmin+$i*$xstep}]
      set y [$funcname $x]
      lappend coords $x $y
   }

   return $coords
}

# circleIntersection --
#    Determine the points where two circles intersect
# Arguments:
#    circle1        X-, Y-coordinate and radius of first circle
#    circle2        X-, Y-coordinate and radius of second circle
# Result:
#    List of x/y coordinates or empty
#
proc ::MathData::circleIntersection { circle1 circle2 } {

   set coords {}

   foreach {x1 y1 r1} $circle1 {break}
   foreach {x2 y2 r2} $circle2 {break}

   #
   # Do we have an intersection?
   #
   set distc [expr {sqrt(($x1-$x2)**2 + ($y1-$y2)**2)}]
   if { $distc**2 <= $r1**2+$r2**2 } {

       set a     [expr {0.5 * ($distc + ($r1**2-$r2**2)/$distc)}]
       set dist  [expr {sqrt($r1**2 - $a**2)}]

       set dx    [expr {$x2-$x1}]
       set dy    [expr {$y2-$y1}]
       set dd    [expr {hypot($dx,$dy)}]

       set xc    [expr {$x1 + $a * $dx/$dd}]
       set yc    [expr {$y1 + $a * $dy/$dd}]

       set xn    [expr {$dist * $dy/$dd}]
       set yn    [expr {-$dist * $dx/$dd}]

       set xp1   [expr {$xc + $xn}]
       set xp2   [expr {$xc - $xn}]
       set yp1   [expr {$yc + $yn}]
       set yp2   [expr {$yc - $yn}]

       set coords [list $xp1 $yp1 $xp2 $yp2]
   }
   return $coords
}

# infiniteLine --
#    Draw a line through two points that extends indefinitely
# Arguments:
#    xp1           X-coordinate first point
#    yp1           Y-coordinate first point
#    xp2           X-coordinate second point
#    yp2           Y-coordinate second point
#    colour        Colour of the line (default: black)
# Result:
#    Canvas ID of the line
#
proc ::MathData::infiniteLine { xp1 yp1 xp2 yp2 {colour black} } {
   variable CNV
   variable SCALE

   set dx [expr {$xp2-$xp1}]
   set dy [expr {$yp2-$yp1}]

   if { abs($dx) > abs($dy) } {
       set xn1    $SCALE(xmin)
       set lambda [expr {($xn1 - $xp1) / $dx}]
       set yn1    [expr {$yp1 + $dy * $lambda}]
       set xn2    $SCALE(xmax)
       set lambda [expr {($xn2 - $xp1) / $dx}]
       set yn2    [expr {$yp1 + $dy * $lambda}]
   } else {
       set yn1    $SCALE(ymin)
       set lambda [expr {($yn1 - $yp1) / $dy}]
       set xn1    [expr {$xp1 + $dx * $lambda}]
       set yn2    $SCALE(ymax)
       set lambda [expr {($yn2 - $yp1) / $dy}]
       set xn2    [expr {$xp1 + $dx * $lambda}]
   }

   polyline [list $xn1 $yn1 $xn2 $yn2] $colour
}

# MathBook --
#    Namespace for the mathbook commands and data
#
namespace eval ::MathBook:: {
   variable count        0
   variable CNV
   variable CNVRIGHT    ""
   variable CNVCODE
   variable TXT
   variable REFRESH
}

# @init --
#    Execute code once (when reading the notebook file)
# Arguments:
#    code            Code to run
# Result:
#    Nothing
#
proc ::MathBook::@init { code } {

   namespace eval ::MathData $code

}

# @canvas --
#    Create a canvas of given size
# Arguments:
#    width            Width in pixels
#    height           Height in pixels
#    code             Code to execute
# Result:
#    Nothing
# Side effect:
#    Canvas created
#
proc ::MathBook::@canvas { width height code } {
   variable CNV
   variable CNVCODE
   variable TXT
   variable count

   incr count

   set CNV             $TXT.cnv$count
   set ::MathData::CNV $CNV

   set CNVCODE($CNV)   $code

   canvas $CNV -width $width -height $height -bg white
   $TXT insert end "\n"
   $TXT window create end -window $CNV
   $TXT insert end "\n"

   namespace eval ::MathData $code

}

# @canvasright --
#    Create a canvas of given size on the right of the text
# Arguments:
#    width            Width in pixels
#    height           Height in pixels
#    code             Code to execute
# Result:
#    Nothing
# Side effect:
#    Canvas created
#
proc ::MathBook::@canvasright { width height code } {
   variable CNV
   variable CNVCODE
   variable CNVRIGHT

   if { $CNVRIGHT eq "" } {
       set CNVRIGHT [canvas .cnv -width $width -height $height -bg white]
       grid configure $CNVRIGHT -row 0 -column 2
   } else {
       $CNVRIGHT configure -width $width -height $height
   }
   set CNV             $CNVRIGHT
   set ::MathData::CNV $CNV

   namespace eval ::MathData $code
}

# @entry --
#    Create an entry widget of given width
# Arguments:
#    name             Name of the associated variable
#    width            Width of the widget (in characters)
# Result:
#    Nothing
# Side effect:
#    Entry created
#
proc ::MathBook::@entry { name width } {
   variable TXT
   variable count

   incr count

   set entry $TXT.entry$count

   entry $entry -textvariable ::MathData::$name -width $width
   $TXT window create end -window $entry

   bind $entry <Return> ::MathBook::Refresh
}

# @label --
#    Create a label widget of given width
# Arguments:
#    name             Name of the associated variable
#    width            Width of the widget (in characters)
# Result:
#    Nothing
# Side effect:
#    Label created
#
proc ::MathBook::@label { name width } {
   variable TXT
   variable count

   incr count

   set label $TXT.label$count

   label $label -textvariable ::MathData::$name -width $width \
                -background white -anchor nw -font $::MathData::fontNormal
   $TXT window create end -window $label
}

# @button --
#    Create a pushbutton
# Arguments:
#    label            Label for the pushbutton
#    code             Code to apply
# Result:
#    Nothing
# Side effect:
#    Button created
#
proc ::MathBook::@button { label code } {
   variable TXT
   variable count
   variable buttoncount

   incr buttoncount

   set button .buttons.button$buttoncount

   button $button -text $label -command "namespace eval ::MathData [list $code]" -width 10
   grid configure $button -column $buttoncount -row 0
}

# @refresh --
#    Define a refresh method - called before the canvas methods
# Arguments:
#    code        Code to be run on refresh
# Result:
#    None
# Side effect:
#    Defines the REFRESH variable
#
proc ::MathBook::@refresh { code } {
   variable REFRESH

   set REFRESH $code
}

# Refresh --
#    Refresh the canvases and labels etc.
# Arguments:
#    None
# Result:
#    None
# Side effect:
#    Canvases refreshed and whatever occurs in the @refresh method
#
proc ::MathBook::Refresh { } {
   variable CNV
   variable CNVCODE
   variable REFRESH
   variable TXT
   variable count

   if { [info exists REFRESH] } {
      namespace eval ::MathData $REFRESH
   }
   foreach {name code} [array get CNVCODE] {
      set ::MathData::CNV $name
      $name delete all
      namespace eval ::MathData $code
   }
}

# initMainWindow --
#    Create the main window
# Arguments:
#    None
# Result:
#    None
# Side effect:
#    Main window created
#
proc ::MathBook::initMainWindow { } {
   variable TXT
   variable count

   set count        0
   set buttoncount -1

   set menu    [menu .mb -type menubar]
   . configure -menu .mb
   .mb add cascade -label File -underline 0 -menu .mb.file
   menu .mb.file -tearoff 0
   .mb.file add command -label Open -command ::MathBook::OpenTextFile
   .mb.file add command -label Exit -command exit

   set tf      .textframe
   set tw      $tf.text
   set buttons .buttons
   set TXT $tw
   set ::MathData::TXT $tw

   frame $tf
   scrollbar $tf.scrollx -orient horiz -command "$tw xview"
   scrollbar $tf.scrolly               -command "$tw yview"
   text      $tw         -yscrollcommand "$tf.scrolly set" \
                         -xscrollcommand "$tf.scrollx set" \
                         -fg black -bg white -font "courier 10" \
                         -wrap word

   grid      $tw         $tf.scrolly
   grid      $tf.scrollx x
   grid      $tw         -sticky news
   grid      $tf.scrolly -sticky ns
   grid      $tf.scrollx -sticky ew

   grid columnconfigure $tf 0 -weight 1
   grid rowconfigure    $tf 0 -weight 1

   frame $buttons
   button $buttons.refresh -text Refresh -command ::MathBook::Refresh -width 10
   grid $tf      -                       -sticky news
   grid $buttons.refresh                 -sticky news
   grid $buttons                         -sticky news

   grid columnconfigure . 0 -weight 1
   grid columnconfigure . 1 -weight 1
   grid rowconfigure    . 0 -weight 1

   $tw tag configure bigbold -font "helvetica 12 bold"
   $tw tag configure normal  -font "courier 10"
   $tw tag configure preform -font "courier 10" -background "lightgrey"
   $tw tag configure indent  -lmargin2 16
}

# fillTextWindow --
#    Fill the text window
# Arguments:
#    filename       Name of the notebook file to use
# Result:
#    None
# Side effect:
#    Text window filled
#
proc ::MathBook::fillTextWindow { filename } {
   variable TXT

   set infile [open $filename "r"]

   set just ""
   set indent ""
   set tag normal

   while { [gets $infile line] >= 0 } {
      set trimmed [string trim $line]

      #
      # Analyse the contents ...
      #
      if { [string first "#" $trimmed] == 0 } {
         continue
      }
      # Ignore empty lines, unless in preformatted text
      if { $trimmed == "" } {
         if { $just != "" } {
            $TXT insert end "\n" $tag
         }
         continue
      }

      if { [string first "@" $trimmed] == 0 } {
         RunWholeCommand $infile $line
         continue
      }

      if { [string first "<h1>" $trimmed] == 0 } {
         set tag bigbold
         set trimmed [string map {<h1> "" </h1> ""} $trimmed]
      }

      if { [string first "<b>" $trimmed] == 0 } {
         set tag [list bold $indent]
         set trimmed [string map {<b> "" </b> ""} $trimmed]
      }

      if { [string first "<i>" $trimmed] == 0 } {
         set tag [list italic $indent]
         set trimmed [string map {<i> "" </i> ""} $trimmed]
      }

      if { [string first "<pre>" $trimmed] == 0 } {
         $TXT insert end "\n"
         set tag  "preform"
         set just "\n"
         continue
      }
      if { [string first "</pre>" $trimmed] == 0 } {
         $TXT insert end "\n"
         set tag  [list normal $indent]
         set just ""
         continue
      }
      if { [string first "<p>" $trimmed] == 0 } {
         $TXT insert end "\n\n"
         continue
      }
      if { [string first "<br>" $trimmed] == 0 } {
         $TXT insert end "\n"
         continue
      }
      if { [string first "<ul>" $trimmed] == 0 } {
         set indent "indent"
         continue
      }
      if { [string first "</ul>" $trimmed] == 0 } {
         $TXT insert end "\n\n"
         set indent ""
         continue
      }
      if { [string first "<li>" $trimmed] == 0 } {
         $TXT insert end "\n* " indent
         continue
      }
      if { [string first "<tag>" $trimmed] == 0 } {
         set tag [list [lindex $tag 0] $indent [string trim [string range $trimmed 5 end]]]
         continue
      }
      if { $just == "" } {
         $TXT insert end "$trimmed " $tag
      } else {
         $TXT insert end "$line\n" $tag
      }
      if { $tag == "bigbold" || $tag == "italic" || $tag == "bold" } {
         set tag "normal"
      }
   }

   close $infile
   $TXT configure -state disabled

   wm title . "$filename - MathBook"
}

# OpenTextFile --
#    Select a text file and display the contents
# Arguments:
#    None
# Result:
#    None
# Side effect:
#    The contents is shown
#
proc ::MathBook::OpenTextFile {} {
    variable TXT
    variable CNVRIGHT

    set types {
        {{Text Files} {*.txt}}
        {{All Files}  *}
    }
    set filename [tk_getOpenFile -filetypes $types -parent . -title "Select mathbook file"]

    if { $filename != "" } {
        $TXT delete 1.0 end
        if { $CNVRIGHT != "" } {
            destroy $CNVRIGHT
        }
        fillTextWindow $filename 
    }
}

# RunWholeCommand --
#    Run an embedded command
# Arguments:
#    infile         Handle to the file
#    line           First line of the command
# Result:
#    None
# Side effect:
#    Whatever the command does
#
proc ::MathBook::RunWholeCommand { infile line } {
   variable TXT

   while { ! [info complete $line] } {
      if { [gets $infile nextline] >= 0 } {
         append line "\n$nextline"
      } else {
         break
      }
   }

   eval $line
}

# main --
#    Get the whole thing going
#
::MathBook::initMainWindow
if { [llength $argv] > 0 } {
    ::MathBook::fillTextWindow [lindex $argv 0]
} else {
    $::MathBook::TXT insert end "-- please open a mathbook file --"
   wm title . "MathBook"
}

JM I may have done something wrong, but I am getting this error when opening the sample file...

 wrong # args: should be "linsert list index element ?element ...?"
 wrong # args: should be "linsert list index element ?element ...?"
    while executing
 "linsert $tag 0"
    (procedure "fillTextWindow" line 81)
    invoked from within
 "fillTextWindow $filename"
    (procedure "::MathBook::OpenTextFile" line 13)
    invoked from within
 "::MathBook::OpenTextFile"

arjen - 2011-12-20 03:11:03

The linsert should be an lindex and the given example does not load properly (i.e. no canvas on the right) so that is something I have to look into. Try:

wish mathbook.tcl tangentcircle.txt

instead. I have _not_ seen the error message you report though. Are you using Tcl/Tk 8.4 or 8.5/8.6?


arjen - 2011-12-20 03:13:53

Just answered my own question: you get this error with Tcl/Tk 8.4. I will have corrected this. I will look at the other problem later.

-- Deletion should be done in the right order - that was the mistake. Corrected.


Jorge - 2011-12-20 22:28:13

correct, I just change to 8.6 and works fine (it still throws error messages with 8.4, ** vs pow for example). Thanks, nice job.


arjen - 2011-12-21 02:58:12

Thanks for the compliment. As for ** versus pow(), I have to add a package require Tcl 8.5 in there.