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:
- pre-coding design phase (meaning I wrote some scribbles on a piece of paper on the way home)
- rapid application development (meaning I was anxious to see it grow, so I ran it as soon as something could show up)
- code reuse by multiple copy-paste-edit cycles (I hardly ever start from scratch, but simply copy an existing file into a new directory)
- visual testing (meaning I tried to see that it did what I wanted it to do)
- HTML-like formatted text to show an explanation
- Tcl code to define entry widgets for editing parameters
- Tcl code to embed a canvas widget with a picture
wish mathbook.tcl example.nb
Example of an input file - store this as a file "example.nb"For other examples: Mathematical notebook - examplesAM 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]
