# ACTIVESTATE TEAPOT-PKG BEGIN TM -*- tcl -*- # -- Tcl Module # @@ Meta Begin # Package diagram::basic 1.0.1 # Meta as::build::date 2015-05-26 # Meta as::origin http://sourceforge.net/projects/tcllib # Meta license BSD # Meta platform tcl # Meta require {Tcl 8.5} # Meta require diagram::point # Meta require {math::geometry 1.1.2} # Meta require snit # @@ Meta End # ACTIVESTATE TEAPOT-PKG BEGIN DECLARE package provide diagram::basic 1.0.1 # ACTIVESTATE TEAPOT-PKG END DECLARE # ACTIVESTATE TEAPOT-PKG END TM ## -*- tcl -*- ## (C) 2010 Andreas Kupries ## BSD Licensed # # ## ### ##### ######## ############# ###################### # # diagram, basic elements (line, arc, box, circle, ellipse, diamond, drum, text) # ## # # ## ### ##### ######## ############# ###################### ## Requisites package require Tcl 8.5 ; # Want the nice things it # brings (dicts, {*}, etc.) package require snit ; # Object framework. package require math::geometry 1.1.2 ; # Vector math (points, line # (segments), poly-lines). package require diagram::point ; # Tagged geometry data and ops # # ## ### ##### ######## ############# ###################### ## Implementation snit::type ::diagram::basic { # # ## ### ##### ######## ############# ###################### ## Public API :: Construction, attach to the specified core. constructor {thecore} { set core $thecore # Basic elements ... First the closed elements (closed curves) ... DefE $core box {textcolor textfont anchor justify stroke style color fillcolor at with width height slant} DefE $core circle {textcolor textfont anchor justify stroke style color fillcolor at with circle::radius} DefE $core diamond {textcolor textfont anchor justify stroke style color fillcolor at with diamond::width diamond::height diamond::aspect} DefE $core drum {textcolor textfont anchor justify stroke style color fillcolor at with width height drum::aspect} DefE $core ellipse {textcolor textfont anchor justify stroke style color fillcolor at with width height} DefE $core text {textcolor textfont anchor justify text at with} # ... and then the open elements (open curves). DefE $core line {textcolor textfont anchor justify stroke style color fillcolor from to then smooth arrowhead noturn} DefE $core arc {textcolor textfont anchor justify stroke style color fillcolor arc::radius clockwise arc::from arc::to} $core new shape arrow $core new shape spline # Note 1: The attribute order is important for arc elements. # We wish to resolve both clockwise and radius before # the from/to points, as we need this data available # for when we have to determine defaults. # Note 2: text elements do not require defaults for width and # height, see the marker (%%) for more information. # ... and their attributes ... # common validation types set dzero [snit::double ${selfns}::D0 -min 0] set dmin [snit::double ${selfns}::D1 -min 0];# 0 exclusive. set izero [snit::integer ${selfns}::I0 -min 0] #set imin [snit::integer ${selfns}::I1 -min 1] # general element style $core new attribute stroke linked {linewidth 1} type $izero $core new attribute style linked {linestyle {}} transform [myproc LineStyle] $core new attribute color linked {linecolor black} $core new attribute fillcolor linked {fillcolor {}} $core new attribute text aggregate 1 $core new attribute textcolor linked {textcolor black} $core new attribute textfont linked {textfont {Helvetica 12}} $core new attribute anchor linked {anchor center} $core new attribute justify linked {justify left} # box geometry, width/height shared with ellipse, drum $core new attribute width linked [Link boxwidth 2 cm] type $dmin $core new attribute height linked [Link boxheight 2 cm] type $dmin $core new attribute slant linked {slant 90} type snit::double;# degrees - range normalization - transform ? # circle geometry $core new attribute arc::radius linked [Link arcradius 1 cm] type $dmin $core new attribute circle::radius linked [Link circleradius 1 cm] type $dmin $core new attribute diameter key circle::radius type $dmin \ transform [myproc CircleRadiusByDiameter] # diamond geometry set dd [mymethod Diamond] $core new attribute diamond::width type $dmin default $dd $core new attribute diamond::height type $dmin default $dd $core new attribute diamond::aspect type $dzero default $dd # drum geometry, width, height, see box. $core new attribute drum::aspect type $dzero linked {drumaspect 0.35} # line style. geometry see core, shared with move command. # Note that chop processing happens in the 'Waypoints' ensemble, in core! $core new attribute chop aggregate 1 type $dzero get [mymethod Chop] $core new attribute arrowhead transform [myproc LineArrows] linked {arrowhead none} $core new attribute smooth type snit::boolean linked {smooth 0} \ get [myproc Smooth] $core new attribute noturn type snit::boolean \ get [myproc NoTurn] #default [myproc NoTurnDefault] # arc location, and direction (counter(clockwise)) set al [mymethod ArcLocation] $core new attribute arc::from type diagram::point default $al $core new attribute arc::to type diagram::point default $al $core new attribute clockwise type snit::boolean linked {clockwise 0} \ get [myproc ClockWise 1] $core new attribute counterclockwise key clockwise type snit::boolean \ get [myproc ClockWise 0] # Further a number of shorthands for some commands and # attributes, and commands using the unicode glyphs looking # like the elements. $core new alias spline {line /shape spline smooth} $core new alias arrow {line /shape arrow arrowhead ->} $core new alias \u21d2 {line /shape arrow arrowhead <-} $core new alias \u27f6 {line /shape arrow arrowhead ->} $core new alias --> {line /shape arrow arrowhead ->} $core new alias <-- {line /shape arrow arrowhead <-} $core new alias <--> {line /shape arrow arrowhead <->} $core new alias O circle $core new alias -- line $core new alias <> diamond $core new alias \u25cb circle $core new alias \u25fb box $core new alias \u25c7 diamond $core new alias \u2312 arc $core new alias \u21b6 arc $core new alias \u21b7 {arc clockwise} $core new alias \u2780 1th $core new alias \u2781 2th $core new alias \u2782 3th $core new alias \u2783 4th $core new alias \u2784 5th $core new alias \u2785 6th $core new alias \u2786 7th $core new alias \u2787 8th $core new alias \u2788 9th $core new alias \u2789 10th $core new alias \u2776 {1th last} $core new alias \u2777 {2th last} $core new alias \u2778 {3th last} $core new alias \u2779 {4th last} $core new alias \u277a {5th last} $core new alias \u277b {6th last} $core new alias \u277c {7th last} $core new alias \u277d {8th last} $core new alias \u277e {9th last} $core new alias \u277f {10th last} # The hooks are run in the specified order, first to last, # until one takes the element, or the system runs out of of # hooks. $core unknown attribute [myproc Styles] $core unknown attribute [myproc Arrowheads] $core unknown attribute [myproc Shorthands] $core unknown attribute [myproc Label] return } # # ## ### ##### ######## ############# ###################### ## Internal :: Register a shape. proc DefE {core name required} { upvar 1 selfns selfns $core new element $name $required [mymethod $name] return } # # ## ### ##### ######## ############# ###################### proc CircleRadiusByDiameter {diameter} { return [expr {double($diameter)/2}] } proc LineStyle {s} { switch -exact -- $s { solid { return {} } dot { return . } dotted { return . } dash { return - } dashed { return - } dash-dot { return -. } dash-dot-dot { return -.. } default { return $s } } } proc Styles {shape words} { set w [{*}$words peek] if {![info exists ourstyles($w)]} {return 0} {*}$words unget style return 1 } proc LineArrows {s} { switch -exact -- $s { start { return first } end { return last } -> { return last } <- { return first } <-> { return both } - { return none } \u21a6 { return last } \u21a4 { return first } \u21ae { return both } default { return $s } } } proc Arrowheads {shape words} { set w [{*}$words peek] if {![info exists ourarrows($w)]} {return 0} {*}$words unget arrowhead return 1 } proc Shorthands {shape words} { set w [{*}$words peek] if {![info exists ourshorts($w)]} {return 0} # Drop the alias name and then stuff the replacement in. {*}$words get foreach str [lreverse $ourshorts($w)] { {*}$words unget $str } return 1 } proc Label {shape words} { # Catch all attribute hook. Register last, as no hook coming # after it will be run. Any unknown attribute is taken to be a # text label associated with the element. {*}$words unget text return 1 } # # ## ### ##### ######## ############# ###################### ## Internal :: Shape implementations. method box {canvas attributes} { array set a $attributes set styling [list \ -fill $a(fillcolor) \ -outline $a(color) \ -width $a(stroke) \ -dash $a(style)] if {$a(slant) != 90} { lassign [BoxSlantedCorners a] corners polygon lappend items [$canvas create polygon \ {*}$polygon {*}$styling] } else { lassign [BoxCorners a] corners rect lappend items [$canvas create rectangle \ {*}$rect {*}$styling] } HandleText $canvas $attributes items [dict get $corners center] return [list $items $corners] } # # ## ### ##### ######## ############# ###################### proc BoxSlantedCorners {av} { upvar 1 $av a lassign [BoxCorners a] corners rect set s $a(slant) set w $a(width) set h $a(height) set dx [expr {cos($s * (4*atan(1))/180.) * $h}] set shift [geo::h $dx] set up [geo::s* -0.5 [geo::v $h]] set right [geo::s* 0.5 [geo::h $w]] set nw [list \ [expr {-$w/2.0}] \ [expr {-$h/2.0}]] set se [list \ [expr { $w/2.0}] \ [expr { $h/2.0}]] # We compute all the corner points as well, given that they # have custom locations. set center {0 0};#[geo::between $nw $se 0.5] if {$dx > 0} { # xc # xnw xn xne # ynw (*)--*---* # / / / # *--<*>--* yc # / / / # *---*--(*) yse # xsw xs xse set vne [geo::+ [geo::+ $right $shift] $up] set northwest $nw set northeast [geo::+ $center $vne] set southeast $se set southwest [geo::- $center $vne] } else { # xc # xnw xn xne # ynw (*)--*---* # \ \ \. # *--<*>--* yc # \ \ \. # *---*--(*) yse # xsw xs xse lassign $nw xnw ynw lassign $se xse yse set northwest [geo::+ $nw $shift] set northeast [geo::p $xse $ynw] set southeast [geo::- $se $shift] set southwest [geo::p $xnw $yse] } set north [geo::between $northwest $northeast 0.5] set east [geo::between $northeast $southeast 0.5] set south [geo::between $southwest $southeast 0.5] set west [geo::between $northwest $southwest 0.5] set polygon [list \ {*}$northwest {*}$northeast \ {*}$southeast {*}$southwest] set corners [list \ north [diagram::point at {*}$north] \ northeast [diagram::point at {*}$northeast] \ east [diagram::point at {*}$east] \ southeast [diagram::point at {*}$southeast] \ south [diagram::point at {*}$south] \ southwest [diagram::point at {*}$southwest] \ west [diagram::point at {*}$west] \ northwest [diagram::point at {*}$northwest] \ center [diagram::point at {*}$center]] return [list $corners $polygon] } proc BoxCorners {av} { upvar 1 $av a # xnw xns # ynw (*)--*---* # | | | # *--<*>--* yew # | | | # *---*--(*) yse # xse set w $a(width) set h $a(height) set rect [list \ [expr {-$w/2.0}] \ [expr {-$h/2.0}] \ [expr { $w/2.0}] \ [expr { $h/2.0}]] return [list [BoxCornersRect $rect] $rect] } proc BoxCornersRect {rect} { lassign $rect xnw ynw xse yse set xns [expr {($xnw + $xse) / 2.0}] set yew [expr {($ynw + $yse) / 2.0}] set w [expr {$xse - $xnw}] set h [expr {$yse - $ynw}] return [list \ north [diagram::point at $xns $ynw] \ northeast [diagram::point at $xse $ynw] \ east [diagram::point at $xse $yew] \ southeast [diagram::point at $xse $yse] \ south [diagram::point at $xns $yse] \ southwest [diagram::point at $xnw $yse] \ west [diagram::point at $xnw $yew] \ northwest [diagram::point at $xnw $ynw] \ center [diagram::point at $xns $yew] \ width $w \ height $h] } # # ## ### ##### ######## ############# ###################### method circle {canvas attributes} { array set a $attributes lassign [CircleCorners a] corners rect lappend items [$canvas create oval {*}$rect \ -fill $a(fillcolor) \ -outline $a(color) \ -width $a(stroke) \ -dash $a(style)] HandleText $canvas $attributes items [dict get $corners center] return [list $items $corners] } # # ## ### ##### ######## ############# ###################### proc CircleCorners {av} { upvar 1 $av a # xnw xns # ynw (*)--*---* # | | | # *--<*>--* yew # | | | # *---*--(*) yse # xse set r $a(circle::radius) set rm [expr {-1 * $r}] set di [expr { 2 * $r}] set rect [list $rm $rm $r $r] # The 90-angles are trivial, no need for big floating of math. set corners [list \ north [diagram::point at 0 $rm] \ east [diagram::point at $r 0] \ south [diagram::point at 0 $r] \ west [diagram::point at $rm 0] \ center [diagram::point at 0 0] \ radius $r \ width $di \ height $di] foreach {dir angle} { northeast 45 southeast -45 southwest -135 northwest 135 } { lappend corners $dir [diagram::point at {*}[geo::s* $r [geo::direction $angle]]] } return [list $corners $rect] } # # ## ### ##### ######## ############# ###################### method ellipse {canvas attributes} { array set a $attributes lassign [EllipseCorners a] corners rect lappend items [$canvas create oval {*}$rect \ -fill $a(fillcolor) \ -outline $a(color) \ -width $a(stroke) \ -dash $a(style)] HandleText $canvas $attributes items [dict get $corners center] return [list $items $corners] } # # ## ### ##### ######## ############# ###################### proc EllipseCorners {av} { upvar 1 $av a # Like CircleCorners, except taking the different radii into account. # ra = w/2 # rb = h/2 set ra [expr {$a(width) / 2.0}] set rb [expr {$a(height) / 2.0}] set rect [list -$ra -$rb $ra $rb] # The 90-degree angles are trivial, no need for floating-point math. set corners [list \ north [diagram::point at 0 -$rb] \ east [diagram::point at $ra 0] \ south [diagram::point at 0 $rb] \ west [diagram::point at -$ra 0] \ center [diagram::point at 0 0] \ width $a(width) \ height $a(height)] # For the 45-degree angles we use precomputed values we just # have to stretch per the actual ellipse radii foreach {dir cos} $ourecos {_ sin} $ouresin { set x [expr {$ra * $cos}] set y [expr {$rb * $sin}] lappend corners $dir [diagram::point at $x $y] } return [list $corners $rect] } # # ## ### ##### ######## ############# ###################### method diamond {canvas attributes} { array set a $attributes lassign [DiamondCorners a] corners poly lappend items [$canvas create polygon {*}$poly \ -fill $a(fillcolor) \ -outline $a(color) \ -width $a(stroke) \ -dash $a(style)] HandleText $canvas $attributes items [dict get $corners center] list $items $corners } # # ## ### ##### ######## ############# ###################### proc DiamondCorners {av} { # * # /|\. # * | * # / \|/ \. # *--<*>--* # \ /|\ / # * | * # \|/ # * upvar 1 $av a set w $a(diamond::width) set h $a(diamond::height) # No calculation of aspect here. This was handled in # DiamondDefaults. Well, in DefaultDiamondGeometry it # delegated this to. set hh [expr {0.5 * $h}] set hw [expr {0.5 * $w}] # Cardinal points. set north [geo::p 0 -$hh] set south [geo::p 0 $hh] set east [geo::p $hw 0] set west [geo::p -$hw 0] # 45-angled points, interpolated between the cardinals. set northeast [geo::between $north $east 0.5] set northwest [geo::between $north $west 0.5] set southeast [geo::between $south $east 0.5] set southwest [geo::between $south $west 0.5] set poly [list {*}$north {*}$east {*}$south {*}$west] set corners [list \ north [diagram::point at {*}$north] \ northeast [diagram::point at {*}$northeast] \ east [diagram::point at {*}$east] \ southeast [diagram::point at {*}$southeast] \ south [diagram::point at {*}$south] \ southwest [diagram::point at {*}$southwest] \ west [diagram::point at {*}$west] \ northwest [diagram::point at {*}$northwest] \ center [diagram::point at 0 0] \ width $w \ height $h] return [list $corners $poly] } # # ## ### ##### ######## ############# ###################### method {Diamond init} {} { # boxwidth, boxheight - Handled by the box attributes. $core state set diamondaspect 2 return } method {Diamond set} {key newvalue} { if {$key ne "diamond::aspect"} return $core state set diamondaspect $newvalue return } method {Diamond fill} {av} { upvar 1 $av attributes # Note: In contrast to box we have to see what we have in toto # before pulling the missing pieces out of the defaults, # because for some combinations the missing data is derived # from what we have. Box otoh can handle each attribute (key) # independently. set hw [dict exists $attributes diamond::width] set hh [dict exists $attributes diamond::height] if {$hw && $hh} { # Both width and height were specified, we can ignore the # aspect, if any. The aspect is implicit in the specified # geometry. return } set ha [dict exists $attributes diamond::aspect] # Pull the known values into locals for quicker access below, # also, and more importantly making the code more readable. if {$hw} { set w [dict get $attributes diamond::width] } if {$hh} { set h [dict get $attributes diamond::height] } if {$ha} { set a [dict get $attributes diamond::aspect] } if {$hw && $ha} { # Derive height from aspect and width. dict set attributes diamond::height [expr {$w / double($a)}] } elseif {$hh && $ha} { # Derive width from aspect and height. dict set attributes diamond::width [expr {$h * $a}] } elseif {$ha} { # Get default width, and derive height. dict set attributes diamond::width [set w [$core state get boxwidth]] dict set attributes diamond::height [expr {$w / double($a)}] } elseif {$hw} { # Get default aspect, and derive height. dict set attributes diamond::aspect [set a [$core state get diamondaspect]] dict set attributes diamond::height [expr {$w / double($a)}] } elseif {$hh} { # Get default aspect, and derive width. dict set attributes diamond::aspect [set a [$core state get diamondaspect]] dict set attributes diamond::width [expr {$h * $a}] } else { # Get defaults for aspect and width, and derive height. dict set attributes diamond::width [set w [$core state get boxwidth]] dict set attributes diamond::aspect [set a [$core state get diamondaspect]] dict set attributes diamond::height [expr {$w / double($a)}] } return } # # ## ### ##### ######## ############# ###################### method drum {canvas attributes} { array set a $attributes lassign [DrumCorners a] corners mbody vlinel vliner top bottom # Main body, background (no outline!) lappend items [$canvas create rectangle {*}$mbody \ -fill $a(fillcolor) \ -outline {}] # Left vertical line of the main drum body lappend items [$canvas create line {*}$vlinel \ -fill $a(color)] # Right vertical line of the main drum body lappend items [$canvas create line {*}$vliner \ -fill $a(color)] # Drum top, full ellipsis lappend items [$canvas create oval {*}$top \ -fill $a(fillcolor) \ -outline $a(color) \ -width $a(stroke) \ -dash $a(style) ] # Drum bottom, background (no outline!) lappend items [$canvas create arc {*}$bottom \ -fill $a(fillcolor) \ -outline {} \ -dash $a(style) \ -start 175 \ -extent 190 \ -style chord] # Drum bottom arc (partial ellipsis, outline only) lappend items [$canvas create arc {*}$bottom \ -fill $a(fillcolor) \ -outline $a(color) \ -width $a(stroke) \ -dash $a(style) \ -start 175 \ -extent 190 \ -style arc ] HandleText $canvas $attributes items [dict get $corners center] return [list $items $corners] } # # ## ### ##### ######## ############# ###################### proc DrumCorners {av} { upvar 1 $av a set w $a(width) set h $a(height) set rect [list \ [expr {-$w/2.0}] \ [expr {-$h/2.0}] \ [expr { $w/2.0}] \ [expr { $h/2.0}]] lassign [geo::nwse $rect] nw se lassign $nw xnw ynw lassign $se xse yse set width $w set height [expr {$h + $a(drum::aspect) * $w}] set hellips [expr {$height * $a(drum::aspect)}] # hellips = as*(h+as*w) = h*as+w*as^2 set center {0 0};#[geo::between $nw $se 0.5] set uphe [geo::s* -0.5 [geo::v $hellips]] set up [geo::s* -0.5 [geo::v $height]] set right [geo::s* 0.5 [geo::h $width]] # topne = center + (up + (uphe + right)) # topsw = center + (up - (uphe + right)) # botne = center - (up - (uphe + right)) # botsw = center - (up + (uphe + right)) set hr [geo::+ $uphe $right] set uhr [geo::+ $up $hr] set dhr [geo::- $up $hr] set topne [geo::+ $center $uhr] set topsw [geo::+ $center $dhr] set botne [geo::- $center $dhr] set botsw [geo::- $center $uhr] # mnw = center + (up - right) # mne = center + (up + right) # mse = center - (up - right) # msw = center - (up + right) set ur [geo::+ $up $right] set dr [geo::- $up $right] set mnw [geo::+ $center $dr] set mne [geo::+ $center $ur] set mse [geo::- $center $dr] set msw [geo::- $center $ur] # Complete corner and rect/poly calculations. set northeast $topne set north [geo::- $topne $right] set northwest [geo::- $topne [geo::s* 2 $right]] set southwest $botsw set south [geo::+ $botsw $right] set southeast [geo::+ $botsw [geo::s* 2 $right]] set east [geo::between $northeast $southeast 0.5] set west [geo::between $northwest $southwest 0.5] set corners [list \ north [diagram::point at {*}$north] \ northeast [diagram::point at {*}$northeast] \ east [diagram::point at {*}$east] \ southeast [diagram::point at {*}$southeast] \ south [diagram::point at {*}$south] \ southwest [diagram::point at {*}$southwest] \ west [diagram::point at {*}$west] \ northwest [diagram::point at {*}$northwest] \ center [diagram::point at {*}$center] \ width $width \ height $height] set mbody [list {*}$mnw {*}$mse] set vlinel [list {*}$mnw {*}$msw] set vliner [list {*}$mne {*}$mse] set top [list {*}$topne {*}$topsw] set bottom [list {*}$botne {*}$botsw] return [list $corners $mbody $vlinel $vliner $top $bottom] } # # ## ### ##### ######## ############# ###################### method text {canvas attributes} { array set a $attributes set label [join $a(text) \n] lappend items [$canvas create text 0 0 \ -text $label \ -font $a(textfont) \ -fill $a(textcolor) \ -justify $a(justify)] # (%%) # The text's box defaults to the canvas item's box. This is # different from the other closed elements, which use standard # values for their defaults, handled by the attribute processor. if {![info exists a(width)]||![info exists a(height)]} { lassign [$canvas bbox [lindex $items end]] xnw ynw xse yse if {![info exists a(width)]} { set a(width) [expr {$xse - $xnw}] } if {![info exists a(height)]} { set a(height) [expr {$yse - $ynw}] } } lassign [BoxCorners a] corners __dummy_rect__ return [list $items $corners] } proc HandleText {canvas attributes iv at} { upvar 1 $iv items self self core core array set a $attributes # Ignore this if there is no text. if {![info exists a(text)]} return # Note: Caller may not have width/height data (open # elements). Force defaults. # At the language level the code here is equivalent to # text justify textcolor with at . # (Where is [last center]). lassign [$self text $canvas $attributes] textitems corners # Now perform a simplified 'relocate' (See diagram::element) # (no sub-elements, ignore the corners, just the one item). # Find current location of the specified corner. set at [diagram::point unbox $at] set with [$core map $corners $a(anchor)] set origin [diagram::point unbox [dict get $corners $with]] # Determine movement vector set delta [geo::- $at $origin] # And do it. foreach i $textitems { $canvas move $i {*}$delta } # At last make the item part of the calling element. lappend items {*}$textitems return } # # ## ### ##### ######## ############# ###################### method line {canvas attributes} { array set a $attributes lassign [LineCorners a] corners poly newdirection lappend items [$canvas create line {*}$poly \ -arrow $a(arrowhead) \ -fill $a(color) \ -smooth $a(smooth) \ -width $a(stroke) \ -dash $a(style) ] # Check for optional shift of line. if {[info exists a(at)]} { set at $a(at) set with [expr {[info exists a(with)] ? $a(with) : "start"}] Relocate $with $at $canvas $items corners } HandleText $canvas $attributes items [dict get $corners center] if {[info exists a(noturn)] && $a(noturn)} { return [list $items $corners absolute] } else { return [list $items $corners absolute $newdirection] } } # # ## ### ##### ######## ############# ###################### proc LineCorners {av} { upvar 1 $av a # Convert waypoints into canvas polyline, generating the basic # corners at the same time. # XXX share parts with basic::move command set poly {} set corners {} set n 1 #puts LC<$a(from)>|<$a(waypoints)>|<$a(to)> lappend corners start [diagram::point at {*}$a(from)] lappend corners end [diagram::point at {*}$a(to)] lappend corners center [diagram::point at {*}[geo::between $a(from) $a(to) 0.5]] foreach p $a(waypoints) { lassign $p x y lappend poly $x $y lappend corners $n [diagram::point at $x $y] incr n } # Lines have trivial corners. The 'end' key is recognized by # the layout engine as a magic overide, it keeps getting used # regardless of the direction turned to. lassign [geo::nwse [lrange $poly end-3 end]] pa pb set direction [geo::octant [geo::- $pb $pa]] return [list $corners $poly $direction] } # # ## ### ##### ######## ############# ###################### proc Smooth {words_dummy} { return 1 } proc NoTurn {words_dummy} { return 1 } #proc NoTurnDefault {args} { return 0 } method Chop {words} { if {![{*}$words size] || ![string is double -strict [set v [{*}$words peek]]]} { return [$core state get circleradius] } {*}$words get return $v } # # ## ### ##### ######## ############# ###################### method arc {canvas attributes} { array set a $attributes #parray a set corners [ArcCorners a] # For debugging purposes, draw a number of helper elements # showing the construction of the arc (from, to, center, # f-c/t-c radii, bounding box, whole circle, and corners). if {0} { lassign $a(rect) w n e s $core draw [subst -nocommands { circle at [$a(center)] radius $a(arc::radius) color black dotted circle at [$a(arc::from)] radius 5 color orange circle at [$a(center)] radius 5 color green circle at [$a(arc::to)] radius 5 color blue line from [$a(arc::from)] then [$a(center)] to [$a(arc::to)] dashed color red line from [$w $n] then [$e $n] then [$e $s] then [$w $s] to [$w $n] color yellow }] foreach {k v} $corners { if {![llength $v] == 2} continue lassign $v cmd detail if {$cmd ne "point"} continue $core draw [subst -nocommands { circle color red radius 3 at [$detail] }] } } # arc start = 0-360, 0 == east, 90 == north. # arc extent = offset from start. lappend items [$canvas create arc {*}$a(rect) \ -start $a(start) \ -extent $a(extent) \ -fill $a(fillcolor) \ -outline $a(color) \ -width $a(stroke) \ -dash $a(style) \ -style arc] HandleText $canvas $attributes items [dict get $corners center] return [list $items $corners absolute $a(direction)] } # # ## ### ##### ######## ############# ###################### proc ArcCorners {av} { upvar 1 $av a core core # Arcs have trivial corners. The 'end' key is recognized by # 'navigation move' as magic overide, it keeps getting used # regardless of the chosen direction. The center is the center # of the arc's circle, and this also provides the compass # points. We only have to provide the proper radius element, # and then translate them per the actual center. set a(circle::radius) $a(arc::radius) set center $a(center) lassign [CircleCorners a] corners __dummy_rect__ set corners [$core move $center $corners] lappend corners \ start [diagram::point at {*}$a(arc::from)] \ end [diagram::point at {*}$a(arc::to)] \ center [diagram::point at {*}$center] return $corners } # # ## ### ##### ######## ############# ###################### method {ArcLocation init} {} {} ; # Nothing to # initialize method {ArcLocation set} {key newvalue} {} ; # in the language # namespace, nor to # set. method {ArcLocation fill} {av} { upvar 1 $av attributes # Bail out quickly when done already. if {[dict exists $attributes center]} return #puts AL|_________________________________________________________________________ array set a $attributes #parray a # Note: We assume that both radius and clockwise have been # resolved already. This means that they have to come before # arc::{from,to} in the list of required attributes (see DefE # calls in the constructor). lassign [$core where] at angle set from $at if {[info exists a(arc::from)]} { set from [diagram::point resolve $from $a(arc::from)] } #puts AL|from|$from| if {![info exists a(arc::to)]} { # Do a (counter)clockwise 90-degree arc beginning at from, # with radius, using the layout engine's current direction # for the baseline. # Note how we are able to directly compute the arc's # center as well. set cangle $angle set tangle $angle set radius $a(arc::radius) if {$a(clockwise)} { incr cangle -90 } else { incr cangle 90 } #puts C/angle\t$cangle #puts T/angle\t$tangle set center [diagram::point resolve $from [diagram::point by $radius $cangle]] set to [diagram::point resolve $center [diagram::point by $radius $tangle]] } else { set to [diagram::point resolve $at $a(arc::to)] #puts AL|to|$to| # Here we know from, to, and radius, and now have to find # the circle's center. That is in essence an intersection # of two circles, around the two points. If the distance # between them is greater than 2*radius we have no center, # strictly speaking. In that case we put the center in the # geometric middle and make it an 180-degree arc, with # adjusted larger radius. set d [geo::distance $from $to] #puts AL|dist|$d|\tr|$a(arc::radius)| if {$d >= (2*$a(arc::radius))} { set center [geo::between $from $to 0.5] set radius [expr {$d/2.}] } else { # Reference # http://local.wasp.uwa.edu.au/~pbourke/geometry/2circle/ set ad [expr {$d/2.}] # a = (r0^2 - r1^2 + d^2 ) / (2 d) |r0==r1 ==> a = d/2 set p [geo::between $from $to 0.5] # P2 = P0 + a/d ( P1 - P0 ) # a/d = (d/2)/d = 1/2 set radius $a(arc::radius) set hd [expr {sqrt($radius*$radius - $ad*$ad)/$d}] # h^2 = r0^2 - a^2, hd = h/d #P3 = center #x3 = x2 +/- h/d ( y1 - y0 ) #y3 = y2 -/+ h/d ( x1 - x0 ) lassign $p mx my ; # P2 lassign $from fx fy ; # P0 lassign $to tx ty ; # P1 if {$a(clockwise)} { set cx [expr {$mx - $hd * ($ty - $fy)}] set cy [expr {$my + $hd * ($tx - $fx)}] } else { set cx [expr {$mx + $hd * ($ty - $fy)}] set cy [expr {$my - $hd * ($tx - $fx)}] } set center [geo::p $cx $cy] } } # We now have to, from, center, and radius for our arc. The # last two pieces are now used to define the bounding box, # i.e. the rectangle we need for the canvas item, and the # from/center, to/center angles define the start and extent # information. set d [geo::p $radius $radius] set nw [geo::- $center $d] set se [geo::+ $center $d] set rect [list {*}$nw {*}$se] # NOTE: The angle proc assumes that positive y is north. The # canvas coordinate system has positive y as south. By # conjugating the point along the y-axis we get the proper # angles for the canvas. set cf [geo::conjy $center] set ff [geo::conjy $from] set tf [geo::conjy $to] set s [math::geometry::angle [list {*}$cf {*}$ff]] set e [math::geometry::angle [list {*}$cf {*}$tf]] # Reorder the angles for direction and use by the canvas. if {$s < 0} { set s [expr {$s + 360}] } if {$e < 0} { set e [expr {$e + 360}] } if {($e < $s) && !$a(clockwise)} { set e [expr {$e + 360}] } if {($s < $e) && $a(clockwise)} { set s [expr {$s + 360}] } #puts start=$s #puts end===$e set direction [geo::octant [geo::- $center $from]] set start $s set extent [expr {$e - $s}] # Save the new state back to the attributes, both original and # derived keys. dict set attributes arc::radius $radius dict set attributes arc::from $from dict set attributes arc::to $to dict set attributes direction $direction dict set attributes rect $rect dict set attributes center $center dict set attributes start $start dict set attributes extent $extent return } # # ## ### ##### ######## ############# ###################### proc ClockWise {v words_dummy} { return $v } proc Link {v n unit} { upvar 1 core core return [list $v [$core unit $n $unit]] } # Factor this with proc 'element::Move' proc Relocate {with at canvas items cv} { upvar 1 $cv corners core core set at [diagram::point unbox $at] set origin [diagram::point unbox [dict get $corners $with]] # Determine movement vector set delta [geo::- $at $origin] # And do it. foreach i $items { $canvas move $i {*}$delta } set corners [$core move $delta $corners] return } # # ## ### ##### ######## ############# ###################### component core ; # diagram core # # ## ### ##### ######## ############# ###################### ## Type construction (pre-computed tables for ellipsis corners) typevariable ouresin typevariable ourecos typeconstructor { ::variable ::math::geometry::torad foreach {dir angle} { northeast 45 southeast -45 southwest -135 northwest 135 } { lappend ourecos $dir [expr { cos($angle * $torad)}] lappend ouresin $dir [expr {- sin($angle * $torad)}] } } typevariable ourstyles -array { solid . dot . dotted . dash . dashed . dash-dot . dash-dot-dot . } typevariable ourarrows -array { start . end . -> . <- . <-> . - . \u21a6 . \u21a4 . \u21ae . } typevariable ourshorts -array { cw clockwise \u21bb clockwise ccw counterclockwise \u21ba counterclockwise wid width ht height rad radius diam diameter \u2300 diameter ljust {anchor west} rjust {anchor east} above {anchor south} below {anchor north} } ## # # ## ### ##### ######## ############# ###################### } namespace eval ::diagram::basic::geo { namespace import ::math::geometry::* } # # ## ### ##### ######## ############# ###################### ## Ready package provide diagram::basic 1.0.1