[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 #

Using compasses and straightedge

The classical tools for geometrical constructions, such as the hexagon or the bissectrix of an angle are the compasses and the straightedge.

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).

First we draw the point P and the circle C (see the canvas on the right-hand side)

Then we take the following steps (press the Next button):

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 } ====== **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 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 ::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 "

" $trimmed] == 0 } { set tag bigbold set trimmed [string map {

""

""} $trimmed] } if { [string first "" $trimmed] == 0 } { set tag [list bold $indent] set trimmed [string map { "" ""} $trimmed] } if { [string first "" $trimmed] == 0 } { set tag [list italic $indent] set trimmed [string map { "" ""} $trimmed] } if { [string first "
" $trimmed] == 0 } {
         $TXT insert end "\n"
         set tag  "preform"
         set just "\n"
         continue
      }
      if { [string first "
" $trimmed] == 0 } { $TXT insert end "\n" set tag [list normal $indent] set just "" continue } if { [string first "

" $trimmed] == 0 } { $TXT insert end "\n\n" continue } if { [string first "
" $trimmed] == 0 } { $TXT insert end "\n" continue } if { [string first "

    " $trimmed] == 0 } { set indent "indent" continue } if { [string first "
" $trimmed] == 0 } { $TXT insert end "\n\n" set indent "" continue } if { [string first "
  • " $trimmed] == 0 } { $TXT insert end "\n* " indent continue } if { [string first "" $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 fillTextWindow $filename if { $CNVRIGHT != "" } { destroy $CNVRIGHT } } } # 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. <>Category Mathematics|Category Application