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