1## -*- tcl -*- 2## (C) 2010 Andreas Kupries <andreas_kupries@users.sourceforge.net> 3## BSD Licensed 4# # ## ### ##### ######## ############# ###################### 5 6# 7# diagram core, using direction and element databases, plus layout 8# engine. Implements the base language (concrete attributes and 9# elements are specified outside, the core only has the pertinent 10# extensibility features). 11# 12# Uses an instance specific namespace to encapsulate the commands of 13# the drawing language, and the drawing state (variables for points, 14# elements, etc.). 15# 16 17## 18# # ## ### ##### ######## ############# ###################### 19## Requisites 20 21package require Tcl 8.5 ; # Want the nice things it 22 # brings (dicts, {*}, etc.) 23package require Tk 24package require snit ; # Object framework. 25package require diagram::direction ; # Database of named directions 26package require diagram::element ; # Database of drawn elements 27package require diagram::navigation ; # State of automatic layouting 28package require diagram::point ; # Point validation and processing. 29package require diagram::attribute ; # Database of element attributes 30package require namespacex ; # Namespace utility functions 31package require struct::set ; # Set arithemetics (blocks) 32package require math::geometry 1.1.2 ; # Vector math (points, line 33 # (segments), poly-lines). 34 35# # ## ### ##### ######## ############# ###################### 36## Implementation 37 38snit::type ::diagram::core { 39 40 # # ## ### ##### ######## ############# ###################### 41 ## Public API :: Core extensibility (drawing elements, attributes, 42 ## special attribute forms) 43 44 method {new direction} {name args} { 45 $dir new direction $name {*}$args 46 return 47 } 48 49 method {new shape} {name} { 50 $elm shape $name 51 return 52 } 53 54 method {new element} {name attrcmd drawcmd} { 55 $elm shape $name 56 $self new alias $name [mymethod Element $name $attrcmd $drawcmd] 57 return 58 } 59 60 method {new alias} {name cmdprefix} { 61 #$self new command $name args "$cmdprefix {*}\$args" 62 $self new command $name args "uplevel 1 \[list $cmdprefix {*}\$args\]" 63 return 64 } 65 66 method {new command} {name arguments body} { 67 proc ${mylangns}::$name $arguments $body 68 return 69 } 70 71 method {new attribute} {name args} { 72 $att new $name {*}$args 73 return 74 } 75 76 method {unknown attribute} {hook} { 77 $att unknown + $hook 78 return 79 } 80 81 # # ## ### ##### ######## ############# ###################### 82 ## 83 84 method snap {} { 85 return [namespacex state get $mylangns] 86 } 87 88 method restore {state} { 89 return [namespacex state set $mylangns $state] 90 } 91 92 # # ## ### ##### ######## ############# ###################### 93 ## Public API :: Draw 94 95 method draw {script} { 96 #set script [list block $script with nw at [diagram::point at 0 0]] 97 return [uplevel 1 [list namespace eval $mylangns $script]] 98 } 99 100 # # ## ### ##### ######## ############# ###################### 101 ## Public API :: Instance construction 102 103 constructor {canvas args} { 104 set mycanvas $canvas 105 set mylangns ${selfns}::$ourlang 106 107 install dir using ::diagram::direction ${selfns}::DIR 108 install elm using ::diagram::element ${selfns}::ELM $dir 109 install nav using ::diagram::navigation ${selfns}::NAV $dir 110 install att using ::diagram::attribute ${selfns}::ATT $self 111 112 $self SetupLanguage 113 114 if {![llength $args]} return 115 $self draw $args 116 return 117 } 118 119 destructor { 120 if {$mycanvas eq {}} return 121 122 # This object has not been detached from the drawing engine 123 # (canvas), therefor its destruction implies the destruction 124 # of the drawn diagram as well. 125 catch { 126 $self drop 127 } 128 return 129 } 130 131 method detach {} { 132 set mycanvas {} 133 return 134 } 135 136 method drop {} { 137 # Destroy all elements and their items. 138 $mycanvas delete {*}[$elm items {*}[$elm elements]] 139 $elm drop 140 $nav reset 141 return 142 } 143 144 # # ## ### ##### ######## ############# 145 146 method {state set} {varname value} { 147 #puts \tState($varname):=|$value| 148 149 namespace upvar $mylangns $varname x 150 set x $value 151 return 152 } 153 154 method {state get} {varname} { 155 namespace upvar $mylangns $varname x 156 157 #puts \tState($varname)->|$x| 158 return $x 159 } 160 161 # # ## ### ##### ######## ############# 162 163 method where {} { 164 return [list [$nav at] [$dir get [$nav direction] angle]] 165 } 166 167 method move {delta corners} { 168 return [$elm move $delta $corners] 169 } 170 171 method map {corners c} { 172 return [$dir map $corners $c] 173 } 174 175 # # ## ### ##### ######## ############# 176 ## Internal :: Setup of core language 177 178 method SetupLanguage {} { 179 # Language encapsulation 180 namespace eval $mylangns {} 181 182 # Standard elements and operations 183 184 $self new alias set [mymethod Set] 185 $self new alias unset [mymethod Unset] 186 $self new alias move [mymethod Move] 187 $self new alias block [mymethod Block] 188 $self new alias group [mymethod Group] 189 $self new alias here [mymethod At] 190 $self new alias direction [list $nav direction] 191 $self new alias by [mymethod By] 192 $self new alias intersect [mymethod Intersect] 193 194 $elm shape move 195 $elm shape block 196 197 # Standard attributes (element appearance, location). 198 199 # keep here ... / type == snit validation type! 200 201 $att new movelength type {snit::double -min 1} linked [list movelength [Unit 2 cm]] 202 203 # XXX refactor the mymethod calls out, use variables 204 $att new with default [mymethod Placement] 205 $att new at type diagram::point transform [mymethod DerefElement] default [mymethod Placement] 206 $att new from type diagram::point transform [mymethod DerefElement] default [mymethod Waypoints] 207 $att new to type diagram::point transform [mymethod DerefElement] default [mymethod Waypoints] 208 $att new then type diagram::point transform [mymethod DerefElement] default [mymethod Waypoints] \ 209 get [mymethod GetPoints] aggregate 1 210 211 $att unknown + [mymethod Directions] 212 213 # Now special forms of commands, handled via 'namespace 214 # unknown'. Making, for example, elements and points into 215 # pseudo-objects. 216 217 namespacex hook add $mylangns [mymethod CatchAll] 218 219 # syntax: [<direction>] --> () 220 namespacex hook on $mylangns [mymethod DCGuard] [mymethod DCRun] 221 222 # Global commands for named directions. The commands are 223 # created on first use. That allows extension packages 224 # declaring their own directions to do this after the core has 225 # booted. Just creating the direction commands at boot time 226 # will miss the directions of extensions. 227 228 # (%%) Commands to access the history (n'th ...) 229 230 # Visible syntax: 231 # 232 # <n>th <shape> ?<corner>? | 2/3 | (1) 233 # <n>th last <shape> ?<corner>? | 3/4 | (2) 234 # last <shape> ?<corner>? | 2/3 | (3) 235 # <n>th last ?<corner>? | 2/3 | (4) 236 # last ?<corner>? | 1/2 | (5) 237 # 238 # Note: The form <shape> ?<corner>? is NOT possible. 239 # <shape> is the drawing command. 240 # 241 # Note 2: Because of (xx) the internal syntax is simpler, as 242 # the argument <n>th is always present, and not 243 # optional. 244 # 245 # <n>th <shape> ?<corner>? | 2-3 246 # <n>th last <shape> ?<corner>? | 3-4 247 # <n>th last ?<corner>? | 2-3 248 # 249 250 $self new alias 1st 1th 251 $self new alias 2nd 2th 252 $self new alias 3rd 3th 253 $self new alias last [mymethod Recall 1th last] ; # (xx) 254 namespacex hook on $mylangns [mymethod RecallGuard] [mymethod Recall] 255 256 # Pseudo object commands for points 257 # 258 # syntax: [<number> cm|mm|point|inch] --> <number> 259 # syntax: [<number> <number>] --> <point> 260 # syntax: [<number> between <point> <point>] --> <point> 261 # syntax: [<point> by <distance> <direction>] --> <point> 262 # syntax: [<point> +|- <point>] --> <point> 263 264 namespacex hook on $mylangns [myproc IsUnit] [myproc Unit] 265 namespacex hook on $mylangns [myproc IsPointCons] {diagram::point at} 266 namespacex hook on $mylangns [myproc IsInterpolation] [mymethod Interpolation] 267 namespacex hook on $mylangns [mymethod IsPointArithBy] [mymethod PointArithBy] 268 namespacex hook on $mylangns [myproc IsPointArithOp] [mymethod PointArithOp] 269 270 # Pseudo object commands for elements. 271 # 272 # syntax: [<element> ?<corner>...? ?names ?<pattern>??] --> <point>|<element>|... 273 274 namespacex hook on $mylangns [myproc IsElementOp] [mymethod ElementOp] 275 return 276 } 277 278 # # ## ### ##### ######## ############# ###################### 279 ## Internal :: Implementation of the core language commands. 280 281 method CatchAll {args} { 282 #puts |||$args||| 283 # Unknown commands are compiled as text elements 284 # --> Calls out into basic, assumes its presence. 285 return [$self draw [list text text {*}$args]] 286 } 287 288 method Move {args} { 289 set attributes [$att attributes move $args {from to then}] 290 set w [dict get $attributes waypoints] 291 292 # XXX share corner generation with line - sub packages. 293 lappend corners start [diagram::point at {*}[lindex $w 0]] 294 lappend corners end [diagram::point at {*}[lindex $w end]] 295 set n 1 296 foreach p $w { 297 lappend corners $n [diagram::point at {*}$p] 298 incr n 299 } 300 301 # note: move is a bit special. It has neither child elements, 302 # nor canvas items. We define it actually only to make it 303 # visible in the history, and to block corner creation. 304 set eid [$elm new move $corners {} {}] 305 $nav move $corners 306 return $eid 307 } 308 309 method Set {args} { 310 #puts SET|$args| 311 # Run builtin for the regular behaviour of the intercepted command. 312 313 set result [uplevel 1 [list ::set {*}$args]] 314 315 # During block processing we save variable re-definitions as 316 # the block's corners 317 if {$myinblock && ([llength $args] == 2)} { 318 lappend mycorners {*}$args 319 } 320 return $result 321 } 322 323 method Unset {args} { 324 #puts UNSET|$args| 325 # Run builtin for the regular behaviour of the intercepted command. 326 327 set result [uplevel 1 [list ::unset {*}$args]] 328 329 # During block processing we are saving variable 330 # re-definitions as the block's corners, so have to remove 331 # that definition too. 332 if {$myinblock} { 333 foreach c $args { 334 dict unset mycorners $c 335 } 336 } 337 return $result 338 } 339 340 method Block {script args} { 341 # args = attributes. 342 343 # Save current state 344 set old [$elm elements] 345 set ehi [$elm history get] 346 set lst [namespacex state get $mylangns] 347 $nav save 348 349 # Process the attributes, and store the changed settings into 350 # their linked variables (if any), to make them proper 351 # defaults inside of the block. 352 353 set attributes [$att attributes block $args {at with}] 354 $att set $attributes 355 set at [dict get $attributes at] 356 set with [dict get $attributes with] 357 358 # Run the block definition, prepare for the capture of corners. 359 set inblock $myinblock 360 set myinblock 1 361 set mycorners {} 362 363 #$self draw $script 364 uplevel 1 $script 365 366 # Remember the captured corners and reset capture system. 367 set myinblock $inblock 368 set corners [dict merge $mycorners] 369 set mycorners {} 370 371 # Extract the set of newly drawn elements. 372 set new [struct::set difference [$elm elements] $old] 373 374 #puts |$new|bb|[$elm bbox {*}$new]| 375 376 # Get the block's bbox from the union of its elements' bboxes. 377 lassign [$elm bbox {*}$new] xnw ynw xse yse 378 379 # XXX see BoxCornersRect of basic, share code 380 set xns [expr {($xnw + $xse) / 2.0}] 381 set yew [expr {($ynw + $yse) / 2.0}] 382 set w [expr {$xse - $xnw}] 383 set h [expr {$yse - $ynw}] 384 385 set compass [list \ 386 north [diagram::point at $xns $ynw] \ 387 northeast [diagram::point at $xse $ynw] \ 388 east [diagram::point at $xse $yew] \ 389 southeast [diagram::point at $xse $yse] \ 390 south [diagram::point at $xns $yse] \ 391 southwest [diagram::point at $xnw $yse] \ 392 west [diagram::point at $xnw $yew] \ 393 northwest [diagram::point at $xnw $ynw] \ 394 center [diagram::point at $xns $yew] \ 395 width $w \ 396 height $h] 397 398 #puts COMPASS|$compass| 399 #puts CORNERS|$corners| 400 401 set corners [dict merge $compass $corners] 402 403 #puts BLOCK__\t($corners) 404 #puts __BLOCK 405 406 # Restore the system state to what it was before we entered 407 # the block. 408 $nav restore 409 namespacex state set $mylangns $lst 410 $elm history set $ehi 411 412 # Now save the block as element, aggregating the children, and 413 # move it into position, based on the placement attributes. 414 set eid [$elm new block $corners {} $new] 415 $elm relocate $eid $at $with $mycanvas 416 $nav move [$elm corners $eid] 417 418 return $eid 419 } 420 421 method Group {script} { 422 # A group is similar to a block, except that only the state of 423 # the layout engine is saved across it, not the whole element 424 # history, etc. The elements created here are not aggregated 425 # either. Further, changes to any attributes made inside the 426 # group are visible after it as well. 427 428 $nav save 429 #$self draw $script 430 uplevel 1 $script 431 $nav restore 432 return 433 } 434 435 method Element {shape required drawcmd args} { 436 # args = attributes. 437 438 # attrcmd :: attr-dict -> attr-dict 439 # drawcmd :: canvas -> attr-dict -> 440 # (attr-dict canvas-item-list corner-dict ?placement-mode ?layout-direction??) 441 442 set newdirection {} 443 set mode {} 444 set attributes [$att attributes $shape $args $required] 445 lassign [{*}$drawcmd $mycanvas $attributes] \ 446 items corners mode newdirection 447 if {$mode eq {}} { set mode relative } 448 449 # Allow the user's commands to override the element type. For 450 # example, an 'arrow' element not only exapnd to 'line 451 # arrowhead ->', but also set the attribute '/shape arrow' to 452 # distinguish them from plain lines in the history. 453 454 if {[dict exists $attributes /shape]} { 455 set shape [dict get $attributes /shape] 456 } 457 458 set eid [$elm new $shape $corners $items {}] 459 460 #puts $shape=$eid\t/mode=$mode/ 461 462 if {$mode eq "relative"} { 463 # Determine the final location of the new element and move 464 # it there, as it was not created at its absolute/final 465 # location already by its drawing command. 466 467 set at [dict get $attributes at] 468 set with [dict get $attributes with] 469 470 #puts "shift such $with at ($at)" 471 $elm relocate $eid $at $with $mycanvas 472 } 473 474 # Update the layout engine with new position, and possibly a 475 # new direction to follow. 476 477 $nav move [$elm corners $eid] ; # This also discards the 478 # intermediate location set 479 # for any turns done during 480 # attribute processing. 481 482 if {$newdirection ne {}} { 483 # The new element changed direction, notify the layout 484 # engine. Commit immediately to the location for the 485 # direction. 486 487 $nav turn $newdirection 1 488 } 489 490 return $eid 491 } 492 493 method At {} { 494 return [diagram::point at {*}[$nav at]] 495 } 496 497 # # ## ### ##### ######## ############# ###################### 498 499 method Corners {elements} { 500 set results {} 501 foreach e $elements { 502 foreach {k v} [$elm corners $e] { 503 lappend result $e.$k $v 504 } 505 } 506 return $result 507 } 508 509 # # ## ### ##### ######## ############# ###################### 510 ## Handling of directions as attributes and global commands. 511 512 method Directions {shape words} { 513 #puts AU||$shape|u(([{*}$words peek [{*}$words size]])) 514 515 # Try to process like for a 'then' attribute, and if that 516 # succeeds stuff the result back to run it through the actual 517 # handling of the implicit 'then'. 518 519 if {![catch { 520 $self ProcessPoints $words newdirection 521 } p]} { 522 #puts <<ok|$p>> 523 524 {*}$words unget $p 525 {*}$words unget then 526 527 #puts AU|||x(([{*}$words peek [{*}$words size]])) 528 529 if {$newdirection ne {}} { 530 $nav turn $newdirection 531 } 532 #puts AU|done 533 return 1 534 } 535 536 #puts AU<<$p>> 537 #puts $::errorInfo 538 return 0 539 } 540 541 # syntax: [<direction>] --> () 542 method DCGuard {args} { 543 #puts DCG|$args|[llength $args]| 544 return [expr {([llength $args] == 1) && 545 [$dir isStrict [lindex $args 0]]}] 546 } 547 548 method DCRun {direction} { 549 #puts DCR|$direction| 550 $nav turn $direction 1 551 $self new command $direction {} \ 552 [list $nav turn $direction 1] 553 return 554 } 555 556 # # ## ### ##### ######## ############# ###################### 557 558 method RecallGuard {args} { 559 #puts RecallGuard|$args|[llength $args]|[regexp {(\d+)th} [lindex $args 0]] 560 return [regexp {(\d+)th} [lindex $args 0]] 561 } 562 method Recall {offset args} { 563 #puts RECALL|$offset|$args|______________________________________________________________ 564 565 # Syntax (internal!). See comments at (%%) in this file for 566 # the differences between internal and user visible syntax, 567 # and how the translation is made. 568 # 569 # <n>th <shape> ?<corner>? | 2-3 | 1-2 | (a) 570 # <n>th last <shape> ?<corner>? | 3-4 | 2-3 | (b) 571 # <n>th last ?<corner>? | 2-3 | 1-2 | (c) 572 # 573 574 set n [llength $args] 575 if {$n < 1 || $n > 3} { 576 return -code error "wrong\#args: should be \"?n'th? ?last? ?shape? ?corner?\"" 577 } 578 579 regexp {(\d+)th} $offset -> offset 580 581 # forward/backward search ? 582 if {[lindex $args 0] eq "last"} { 583 set args [lassign $args _] 584 set offset -$offset 585 } 586 587 # specific shape/all shapes ? 588 if {[$elm isShape [lindex $args 0]]} { 589 set args [lassign $args shape] 590 } else { 591 set shape {} ;# Search all shapes. 592 } 593 594 # corner yes/no ? 595 set corner {} 596 set n [llength $args] 597 if {$n == 1} { 598 lassign $args corner 599 } elseif {$n > 1} { 600 return -code error "wrong\#args: should be \"?n'th? ?last? ?shape? ?corner?\"" 601 } 602 603 #puts H|recall|o|$offset| 604 #puts H|recall|s|$shape| 605 #puts H|recall|c|$corner| 606 607 # ... And access the history files ... 608 609 set eid [$elm history find $shape $offset] 610 611 #puts H|recall|e|$eid| 612 613 # ... at last return result, resolving the corner, if any such 614 # was specified. 615 616 if {$corner ne {}} { 617 #puts H|recall|p|[$elm corner $eid $corner] 618 return [$elm corner $eid $corner] 619 } else { 620 #puts H|recall|x|$eid| 621 return $eid 622 } 623 } 624 625 # # ## ### ##### ######## ############# ###################### 626 627 # syntax: [<number> <unit>] --> <number> 628 proc IsUnit {args} { 629 #puts IsUnit|$args|[llength $args]|[string is double -strict [lindex $args 0]]|[info exists ourunit([lindex $args 1])] 630 return [expr {([llength $args] == 2) && 631 [string is double -strict [lindex $args 0]] && 632 [info exists ourunit([lindex $args 1])]}] 633 } 634 635 proc Unit {n unit} { 636 #puts "Unit $unit ($n)" 637 return [expr {$n * $ourunit($unit)}] 638 } 639 640 method unit {n unit} { return [Unit $n $unit] } 641 642 # syntax: [<number> <number>] --> <point> 643 proc IsPointCons {args} { 644 #puts IsPointCons|$args|[llength $args]|[string is double -strict [lindex $args 0]]|[string is double -strict [lindex $args 1]] 645 return [expr {([llength $args] == 2) && 646 [string is double -strict [lindex $args 0]] && 647 [string is double -strict [lindex $args 1]]}] 648 } 649 650 # syntax: [<number> between <point> <point>] --> <point> 651 proc IsInterpolation {args} { 652 #puts IsInterpolation|$args|[llength $args]|[string is double -strict [lindex $args 0]]|[string is double -strict [lindex $args 1]] 653 return [expr {([llength $args] == 4) && 654 [string is double -strict [lindex $args 0]] && 655 ([lindex $args 1] eq "between") && 656 [diagram::point is [lindex $args 2]] && 657 [diagram::point is [lindex $args 3]]}] 658 } 659 660 method Interpolation {s __between__ a b} { 661 set a [diagram::point resolve [$nav at] $a] 662 set b [diagram::point resolve $a $b] 663 return [diagram::point at {*}[geo::between $a $b $s]] 664 } 665 666 method By {distance direction} { 667 if {[$dir isStrict $direction]} { 668 set angle [$dir get $direction angle] 669 } else { 670 set angle $direction 671 } 672 return [diagram::point by $distance $angle] 673 } 674 675 # syntax: [<point> by <distance> <direction>] --> <point> 676 method IsPointArithBy {args} { 677 #puts IsPointArith|$args|[llength $args]| 678 return [expr {([llength $args] == 4) && 679 [diagram::point is [lindex $args 0]] && 680 ([lindex $args 1] eq "by") && 681 [string is double -strict [lindex $args 2]] && 682 [$dir is [lindex $args 3]]}] 683 } 684 685 method PointArithBy {p __by__ distance direction} { 686 if {[$dir isStrict $direction]} { 687 set angle [$dir get $direction angle] 688 } else { 689 set angle $direction 690 } 691 set delta [diagram::point by $distance $angle] 692 693 #puts PointArith|$p|++|D/$direction|A/$angle|d/$delta|==|[diagram::point + $p $delta]| 694 return [diagram::point + $p $delta] 695 } 696 697 # syntax: [<point> by <distance> <direction>] --> <point> 698 proc IsPointArithOp {args} { 699 #puts IsPointArithOp|$args|[llength $args]| 700 # See ElementOp for similar code. 701 return [expr {([llength $args] == 3) && 702 [diagram::point is [lindex $args 0]] && 703 ([lindex $args 1] in {+ - |}) && 704 [diagram::point is [lindex $args 2]]}] 705 } 706 707 method PointArithOp {pa op pb} { 708 #puts PointArithOp|$pa|$op|$pb|=|[diagram::point $op $pa $pb]| 709 return [diagram::point $op $pa $pb] 710 } 711 712 method Intersect {ea eb} { 713 set pas [diagram::point unbox [$elm corner $ea start]] 714 set pae [diagram::point unbox [$elm corner $ea end]] 715 set pbs [diagram::point unbox [$elm corner $eb start]] 716 set pbe [diagram::point unbox [$elm corner $eb end]] 717 718 #puts |$pas|---|$pae| 719 #puts |$pbs|---|$pbe| 720 721 set linea [list {*}$pas {*}$pae] 722 set lineb [list {*}$pbs {*}$pbe] 723 724 set p [geo::findLineIntersection $linea $lineb] 725 #puts |$p| 726 727 if {$p eq "none"} { 728 return -code error "Intersection failure, parallel lines have none" 729 } elseif {$p eq "coincident"} { 730 return -code error "Intersection failure, unable to choose among infinite set of points of coincident lines" 731 } 732 733 return [diagram::point at {*}$p] 734 } 735 736 # # ## ### ##### ######## ############# ###################### 737 738 # syntax: [<element> ?<corner>...? ?names ?<pattern>??] --> <point>|<element>|... 739 proc IsElementOp {args} { 740 #puts IsElementOp|$args|[llength $args]|[diagram::element is [lindex $args 0]] 741 return [expr {([llength $args] > 1) && 742 [diagram::element is [lindex $args 0]]}] 743 } 744 745 method ElementOp {eid args} { 746 #puts Element|$eid|$corner|=|[$elm corner $eid $corner]| 747 #array set c [$elm corners $eid];parray c 748 749 # See IsPointArithOp guard for similar code. 750 if {([llength $args] == 2) && 751 ([lindex $args 0] in {+ - |}) && 752 [diagram::point is [lindex $args 1]]} { 753 754 # Point arithmetic on an element is based in the 755 # element's center. Resolve and divert. 756 lassign $args op p 757 return [$self PointArithOp [$elm corner $eid center] $op $p] 758 } 759 760 set stop 0 761 foreach operation $args next [lrange $args 1 end] { 762 if {$stop} { 763 if {$stop == 2} { incr stop -1 ; continue } 764 return -code error "wrong#args: should be \"?corner...? ?names ?pattern??\"" 765 } 766 if {$operation eq "names"} { 767 if {$next eq {}} { set next * } 768 set eid [$elm names $eid $next] 769 set stop 2 770 # stop => error out if there is an argument after next 771 } else { 772 set eid [$elm corner $eid $operation] 773 } 774 } 775 return $eid 776 } 777 778 # # ## ### ##### ######## ############# ###################### 779 780 method DerefElement {p} { 781 # Convert element references to the elements' center point. 782 # Used when processing the attributes 'from', 'to', 'then', 783 # and 'at'. 784 785 if {[diagram::element is $p]} { 786 return [dict get [$elm corners $p] center] 787 } else { 788 return $p 789 } 790 } 791 792 # # ## ### ##### ######## ############# ###################### 793 794 method {Placement init} {} {} ; # Nothing to 795 # initialize 796 method {Placement set} {key newvalue} {} ; # in the language 797 # namespace, nor to 798 # set. 799 method {Placement fill} {av} { 800 upvar 1 $av attributes 801 802 if {[dict exists $attributes .withat]} return 803 dict set attributes .withat . 804 805 # at/with - rules 806 # 807 808 # (1) If the user did not specify 'at', nor 'with', then both 809 # are filled with the information from the layout engine. 810 # 811 # (2) If 'with' was specified, but not 'at', then 'at' is 812 # filled from the layout engine. 813 # 814 # (3) If 'at' was specified, but not 'with' then 'with' 815 # defaults to the 'center', and the layout engine is 816 # ignored. 817 # 818 # (4) If both have been specified, then nothing is done. 819 # 820 # (5) The data for 'at' is an untagged absolute location. 821 # A user specified value is a diagram::point/delta. 822 # This is resolved as well. 823 824 if {![dict exists $attributes at]} { 825 dict set attributes at [$nav at] ; # (1,2) 826 if {[dict exists $attributes with]} return 827 dict set attributes with [$nav corner] ; # (1) 828 } else { 829 # (5) User specified location. Resolve to untagged 830 # absolute location. 831 dict set attributes at \ 832 [diagram::point resolve \ 833 [$nav at] [dict get $attributes at]] 834 835 if {![dict exists $attributes with]} { 836 dict set attributes with center ; # (3) 837 } ; # else (4) 838 } 839 return 840 } 841 842 # # ## ### ##### ######## ############# ###################### 843 844 method {Waypoints init} {} {} ; # Nothing to 845 # initialize 846 method {Waypoints set} {key newvalue} {} ; # in the language 847 # namespace, nor to 848 # set. 849 method {Waypoints fill} {av} { 850 upvar 1 $av attributes 851 852 # from/then/to - rules 853 # Bail out quickly when done already. 854 if {[dict exists $attributes waypoints]} return 855 856 # Determine a starting point if not specified, and/or make a 857 # relative specification absolute. 858 859 set awaypoints {} 860 set last [$nav at] ; # absolute location, untagged. 861 862 if {[dict exists $attributes from]} { 863 set last [diagram::point resolve $last [dict get $attributes from]] 864 } 865 866 dict set attributes from $last 867 lappend waypoints $last 868 869 if {[dict exists $attributes then]} { 870 #puts |then|[dict get $attributes then]| 871 foreach p [dict get $attributes then] { 872 #puts \t|$p| 873 set last [diagram::point resolve $last $p] 874 lappend waypoints $last 875 } 876 } 877 878 if {![dict exists $attributes to]} { 879 # Use a default if and only if no intermediate waypoints 880 # have been specified. For if they have, then the last of 881 # the intermediates will serve as the 'to'. 882 883 if {![dict exists $attributes then]} { 884 # Compute a location based on direction and defaults 885 886 set distance [$self state get movelength] 887 set angle [$dir get [$nav direction] angle] 888 set delta [diagram::point by $distance $angle] 889 set last [diagram::point resolve $last $delta] 890 lappend waypoints $last 891 } 892 } else { 893 set last [diagram::point resolve $last [dict get $attributes to]] 894 lappend waypoints $last 895 } 896 897 dict set attributes waypoints $waypoints 898 dict set attributes to $last 899 900 # If chop values have been specified then now is the time to 901 # process their effect on the waypoints. 902 903 if {[dict exists $attributes chop]} { 904 set choplist [dict get $attributes chop] 905 if {[llength $choplist] > 2} { 906 set choplist [lrange $choplist end-1 end] 907 } elseif {[llength $choplist] < 2} { 908 lappend choplist [lindex $choplist end] 909 } 910 911 #puts w|||$waypoints||| 912 #puts c|||$choplist||| 913 914 lassign $choplist chopstart chopend 915 916 # We have to handle multi-segment lines. First we chop 917 # whole segments until the length to chop is less than the 918 # length of the current first/last segment. Note that we 919 # may be left with an empty path. 920 921 while {[llength $waypoints] >= 2} { 922 lassign $waypoints pa pb 923 set seglen [geo::distance $pa $pb] 924 if {$seglen > $chopstart} break 925 set waypoints [lrange $waypoints 1 end] 926 set chopstart [expr {$chopstart - $seglen}] 927 } 928 while {[llength $waypoints] >= 2} { 929 lassign [lrange $waypoints end-1 end] pa pb 930 set seglen [geo::distance $pa $pb] 931 if {$seglen > $chopend} break 932 set waypoints [lrange $waypoints 0 end-1] 933 set chopend [expr {$chopend - $seglen}] 934 } 935 936 #puts w'|||$waypoints||| 937 #puts c'|||$choplist||| 938 939 if {[llength $waypoints] > 2} { 940 # Ok, we have enough segments left, now actually chop 941 # the first and last segments. 942 943 # Relative chop positions, translated to actual 944 # position through interpolation. 945 lassign $waypoints pa pb 946 set s [expr {double($chopstart)/$seglen}] 947 set anew [geo::between $pa $pb $s] 948 949 lassign [lrange $waypoints end-1 end] a b 950 set s [expr {1-double($chopend)/$seglen}] 951 set bnew [geo::between $pa $pb $s] 952 953 set waypoints [lreplace [lreplace $waypoints 0 0 $anew] end end $bnew] 954 955 } elseif {[llength $waypoints] == 2} { 956 # There is only one segment left in the 957 # poly-line. Check that chopping the ends doesn't 958 # leave it empty. 959 960 lassign $waypoints pa pb 961 set seglen [geo::distance $pa $pb] 962 if {($chopstart + $chopend) > $seglen} { 963 set waypoints {} 964 } else { 965 # Relative chop positions. 966 set ss [expr {double($chopstart)/$seglen}] 967 set se [expr {1-double($chopend)/$seglen}] 968 969 #puts s|$ss 970 #puts e|$se 971 972 # Translate to actual position through interpolation. 973 set anew [geo::between $pa $pb $ss] 974 set bnew [geo::between $pa $pb $se] 975 976 set waypoints [list $anew $bnew] 977 } 978 } else { 979 set waypoints {} 980 } 981 982 dict set attributes waypoints $waypoints 983 dict set attributes from [lindex $waypoints 0] 984 dict set attributes to [lindex $waypoints end] 985 } 986 987 # Note: Keeping from, and to. direct access to these points 988 # could be beneficial. 989 990 #puts WP 991 #puts ______________________________________________________ 992 #array set a $attributes ; parray a 993 #puts ______________________________________________________ 994 995 return 996 } 997 998 method GetPoints {words} { 999 set p [$self ProcessPoints $words newdirection] 1000 if {$newdirection ne {}} { 1001 $nav turn $newdirection 1002 } 1003 return $p 1004 } 1005 1006 method ProcessPoints {words nv} { 1007 upvar 1 $nv newdirection 1008 set newdirection {} 1009 1010 # words = P ... !P 1011 # P = <point> 1012 # | <directionname> <double> 1013 # | <directionname> 1014 1015 if {![{*}$words size]} { 1016 return -code error "wrong\#args, expected a point" 1017 } 1018 1019 set p [{*}$words peek] 1020 if {[diagram::point is $p]} { 1021 # Got an immediate location (absolute or relative). As we 1022 # expect only one of such we stop processing input and 1023 # return. 1024 1025 {*}$words get 1026 return $p 1027 } 1028 1029 # Not a proper location. Check if we have a series 1030 # of <direction> ?<distance>? words. 1031 1032 set point [diagram::point delta 0 0] 1033 set resok 0 1034 1035 while {[{*}$words size]} { 1036 1037 set p [{*}$words peek] 1038 if {![$dir isStrict $p]} { 1039 # Not a direction. If we had delta specs before then 1040 # we just have found the end and can stop processing. 1041 # Otherwise there was no spec at at all, which is an 1042 # error. 1043 break 1044 } 1045 1046 set direction [$dir validate $p] 1047 1048 # We have a direction, check if there is a distance coming 1049 # after, then add to the sum of previous deltas, 1050 # i.e. integrate the path. 1051 1052 {*}$words get 1053 if {[{*}$words size] && [string is double -strict [{*}$words peek]]} { 1054 set distance [{*}$words get] 1055 } else { 1056 set distance [$self state get movelength] 1057 } 1058 1059 set angle [$dir get $direction angle] 1060 set v [diagram::point by $distance $angle] 1061 set point [diagram::point + $point $v] 1062 set resok 1 1063 1064 # Keep track of the last direction used. When we are done 1065 # the caller will push this to the layout engine, so that 1066 # it tracks turns specified in the attributes of an 1067 # element. 1068 1069 set newdirection $direction 1070 } 1071 1072 if {$resok} { 1073 return $point 1074 } else { 1075 return -code error "Expected point/delta specification, got \"$p\"" 1076 } 1077 } 1078 1079 # # ## ### ##### ######## ############# ###################### 1080 ## Instance data, database tables as arrays, keyed by direction 1081 ## and alias names. 1082 1083 variable mycanvas {} ; # Drawing backend 1084 variable mylangns {} ; # Name of the namespace holding the drawing state. 1085 1086 variable myinblock 0 ; # Boolean flag. True when processing a block. 1087 variable mycorners {} ; # Corner dictionary during block processing. 1088 1089 component dir ; # Knowledge base of named directions. 1090 component elm ; # Database of drawn elements. 1091 component nav ; # State of automatic layout engine 1092 component att ; # Database of attributes 1093 1094 typevariable ourlang LANG 1095 1096 typevariable ourunit -array {} ; # database for unit conversion 1097 1098 typeconstructor { 1099 # [tk scaling] is in pixels/point, with point defined as 1/72 inch 1100 foreach {unit s} { 1101 mm 2.83464566929 1102 cm 28.3464566929 1103 inch 72 1104 point 1 1105 } { 1106 set ourunit($unit) [expr {$s * [tk scaling]}] 1107 } 1108 } 1109 1110 ## 1111 # # ## ### ##### ######## ############# ###################### 1112} 1113 1114# # ## ### ##### ######## ############# ###################### 1115## Ready 1116 1117namespace eval ::diagram::core::geo { 1118 namespace import ::math::geometry::* 1119} 1120 1121package provide diagram::core 1 1122