# bike.tcl # This program computes the effective wheel diameter of a bike given its # gear setup. For example, if the effective diameter is 100 inches, it is # as if you have an old-fashioned bike with a 100 inch diameter front wheel # and the pedals are connected right to its axle. The bigger the diameter # the harder it is to pedal. option add *Entry.width 2 option add *background gray75 option add *highlightBackground gray75 . config -borderwidth 10 -bg gray75 if [info exists embed_args] { # Don't resize within plugin grid propagate . false set maxwidth $embed_args(width) } else { set maxwidth 400 } message .msg -text "Bicycle Gear Ratios" -aspect 2000 # Every text entry widget can be associated with a Tcl variable. In these cases # front(n) and back(n) are the number of gears at the front and back of the bike. entry .efront -textvariable front(n) entry .eback -textvariable back(n) label .lfront -text "# front gears" label .lback -text "# back gears" entry .size -textvariable size label .lsize -text "Wheel diameter" set size 27 # Hitting Return in a text entry recomputes the graph. foreach e {.size .efront .eback} { bind $e UpdateRatios bind $e UpdateRatios bind $e {%W select from 0 ; %W select to end} } button .doit -text Recompute -command UpdateRatios # The grid geometry manager is used for the main arrangement of widgets grid .msg -columnspan 3 -sticky news grid .lfront .efront -sticky w grid .lback .eback -sticky w grid .lsize .size .doit -sticky w grid .doit -sticky e # When the number of gears is changed, the set of entries for each gear is re-created. trace variable front(n) w UpdateFront trace variable back(n) w UpdateBack proc UpdateFront {args} { UpdateBoxes .front 1 front ff0000 110000 } proc UpdateBack {args} { UpdateBoxes .back 2 back 0000ff 000011 } proc UpdateBoxes {frame row varName color dColor} { # upvar creates a local name alias for a global variable (absolute scope #0) # In this case, 'var' is an alias for 'front' or 'back' upvar #0 $varName var catch {destroy $frame} if ![NumberOk var(n) 1 10] { return } # Make a horizontal stack of a label and several entries, using the packer frame $frame label $frame.teeth -text "# teeth" pack $frame.teeth -side left for {set i 0} {$i < $var(n)} {incr i} { entry $frame.$i -textvariable $varName\($i) -foreground #$color # Fade the text color from bright to black set color [format %06x [expr 0x$color - 0x$dColor]] pack $frame.$i -side left bind $frame.$i UpdateRatios bind $frame.$i UpdateRatios bind $frame.$i {%W select from 0 ; %W select to end} } # Position the horizontal stack in the main grid of widgets grid $frame -column 2 -row $row -sticky w } # Validate a number. Returns 0 if there isn't a number in the variable. proc NumberOk {varName min max} { upvar $varName var if {([string length [string trim $var]] == 0)} { return 0 } if {[catch {expr $var}]} { set var "" return 0 } if {$var < $min} { set var $min } if {$var > $max} { set var $max } return 1 } # Set default values for the front and back gear configuration. array set front { 0 27 1 40 2 50 } set front(n) [expr [array size front] -1] array set back { 0 27 1 21 2 19 3 17 4 15 5 13 } set back(n) [expr [array size back] -1] # UpdateRatios displays a graph of the effective wheel diameter. proc UpdateRatios {} { global front back # Find the minimum and maximum gear sizes in front and back. foreach name {front back} { # Here upvar creates an alias for a local variable (scope 0 frames up) upvar 0 $name array catch {unset array(max)} catch {unset array(min)} for {set n 0} {$n < $array(n)} {incr n} { if {([string length [string trim $array($n)]] == 0) || [catch {expr $array($n)}]} { continue } if {![info exists array(min)] || ($array($n) < $array(min)) } { set array(min) $array($n) } if {![info exists array(max)] || ($array($n) > $array(max)) } { set array(max) $array($n) } } if {![info exists array(max)] || ![info exists array(min)]} { return } } # Find the bounds of the graph, rounded to 5 set min [expr [Gearage $front(min) $back(max)]/5 * 5] set max [expr ([Gearage $front(max) $back(min)]+5)/5 * 5] set span [expr $max - $min] if [winfo exists .c] { .c delete all } else { canvas .c } global maxwidth set y 10 set xoff 20 set xscale [expr double($maxwidth-$xoff-35)/$span] set fcolor ff0000 set df 110000 for {set f 0} {$f < $front(n)} {incr f} { if ![NumberOk front($f) 3 1000] { continue } # Create a horizontal line for each front gear, same color as text entry. .c create text 0 $y -text $front($f) -fill #$fcolor .c create line $xoff $y [expr $xscale*$span + $xoff] $y -fill #$fcolor set fcolor [format %06x [expr 0x$fcolor - 0x$df]] set bcolor 0000ff for {set b 0} {$b < $back(n)} {incr b} { if ![NumberOk back($b) 3 1000] { continue } if [catch {Gearage $front($f) $back($b)} g] { continue } # Plot the points in the same color as rear wheel text. set x [expr $xscale *($g-$min) +$xoff] set id [.c create oval [expr $x-3] [expr $y-3] [expr $x+3] [expr $y+3] \ -fill #$bcolor -tags "gearage=$g f=$f b=$b"] set bcolor [format %06x [expr 0x$bcolor - 0x11]] } incr y 20 } bind .c [list FeedbackOn %x %y] bind .c [list FeedbackOn %x %y] bind .c [list FeedbackOff] .c create text $xoff $y -text $min -anchor w .c create text [expr $xscale*$span+$xoff] $y -text $max -anchor e .c create text [expr ($xscale*$span+$xoff)/2] $y \ -text "Effective Wheel Diameter" -anchor c # Size the canvas to fit the plot .c move all 10 10 foreach {x1 y1 x2 y2} [.c bbox all] {} .c config -width [expr $x2 + 0] -height [expr $y2 +0] grid .c -row 4 -column 0 -columnspan 3 } proc Gearage {f b} { global size expr round($f.0 / $b.0 * $size) } # As the mouse moves over the plot, display the exact value for each point it crosses. proc FeedbackOn {x y} { global feedback foreach id [.c find overlapping [expr $x-2] [expr $y-2] [expr $x+2] [expr $y+2]] { if [regexp {gearage=([0-9]+) f=([0-9]+) b=([0-9]+)} [.c itemconfigure $id -tags] junk g f b] { FeedbackOff foreach {x1 y1 x2 y2} [.c coords $id] {} set feedback [.c create text $x1 $y1 -text $g -fill blue -anchor s] .front.$f config -bg white .back.$b config -bg white lappend feedback $f $b break } } } proc FeedbackOff {} { global feedback catch {.c delete [lindex $feedback 0]} catch {.front.[lindex $feedback 1] config -bg grey75} catch {.back.[lindex $feedback 2] config -bg grey75} } update ;# So auto-sizing works ok UpdateRatios .