Arjen Markus (18 december 2003) I am mildly proud to present a first (underdocumented) version of a mathematical notebook.
LV Is this in any way related to your tclmath.kit mathematical workbench work?
AM Yes, in a manner of speaking: it is another way of working with mathematical subjects in Tcl. This time, a bit more educational/presentational. I guess I am looking for the right form.
After at least 10000 seconds of hard labour, meticulous programming and self-defying testing, it works! I had to use the most cunning techniques I could muster:
This was all needed to meet my deadline: the end of the evening.
But I did it!
Below you will find the first fruits of this herculean achievement. Enjoy!
Okay, what is it? It is a viewer for files that I call "mathematical notes":
The example will make it a bit clearer.
Note: this is not only meant for the mathematically inclined, you can set up any kind of "exercise book" you like.
This being the first version, its user-interface is limited: You will have to use a command-line like:
wish mathbook.tcl example.nb
Example of an input file - store this as a file "example.nb"
For other examples: Mathematical notebook - examples
AM I updated the code below dd 22 december 2003.
AM Here is a new version - Mathematical notebook revisited
# Example of a mathematical note to show the possibilities # # Note: # The number of HTML-like tags is kept to a bare minimum: # - <h1> big letters (the whole line; </h1> ignored) # - <p> for a new paragraph # - <br> to break the line # - <pre> and </pre> to indicate preformatted text # - leading blanks are significant # # These tags are only recognised at the start of the line! # # Initialisation: define a simple function # @init { proc pol3 {x} { variable a expr {1.0+$a*$x+$x*$x*$x} } set a 0.0 set xmin -5.0 set xmax 5.0 } <h1>Third-degree polynomial functions</h1> <p> This note lets you inspect the following function: <pre> f(x) = 1.0 + a*x + x^3 </pre> You can fill in the parameter a: @entry a 10 <p> You can also change the horizontal limits of the graph: <p> Minimum x: @entry xmin 10 <br> Maximum x: @entry xmax 10 <p> By changing the value of "a" and pressing the Refresh button (or the Enter key), you update the graph: <p> @canvas 300 200 { set nosteps 50 set data [func pol3 $xmin $xmax $nosteps] scale $data axes polyline $data black polyline [list $xmin $xmin $xmax $xmax] red text 0.0 0.0 "Origin" } Note: the red line is the line "y = x"
End of example
Here is the code:
# 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 Tk # Slightly updated version of gtklook.tcl included 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 "" } # 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: # None # 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 } # text -- # Draw a text string at a given position # Arguments: # x X coordinate # y Y coordinate # string String to show # Result: # None # 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)/double($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 } # MathBook -- # Namespace for the mathbook commands and data # namespace eval ::MathBook:: { variable count 0 variable CNV variable TXT } # @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 } # @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: # Entry 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 "Courier 10" $TXT window create end -window $label } # @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 tf .textframe set tw $tf.text set 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 button .refresh -text Refresh -command ::MathBook::Refresh -width 10 button .exit -text Exit -command exit -width 10 grid $tf - -sticky news grid .refresh .exit -sticky nw 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" } # 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 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 "<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 "normal" 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 { $just == "" } { $TXT insert end "$trimmed " $tag } else { $TXT insert end "$line\n" $tag } if { $tag == "bigbold" } { set tag "normal" } } close $infile $TXT configure -state disabled } # 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 ::MathBook::fillTextWindow [lindex $argv 0]