# ACTIVESTATE TEAPOT-PKG BEGIN TM -*- tcl -*- # -- Tcl Module # @@ Meta Begin # Package diagram::element 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::element 1 # ACTIVESTATE TEAPOT-PKG END DECLARE # ACTIVESTATE TEAPOT-PKG END TM ## -*- tcl -*- ## (C) 2010 Andreas Kupries ## BSD Licensed # # ## ### ##### ######## ############# ###################### # # Database of the created/drawn elements, with their canvas items, # corners (named points), and sub-elements. # ## # # ## ### ##### ######## ############# ###################### ## 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 # # ## ### ##### ######## ############# ###################### ## Implementation snit::type ::diagram::element { # # ## ### ##### ######## ############# ###################### typemethod validate {id} { if {[$type is $id]} {return $id} return -code error "Expected element id, got \"$id\"" } typemethod is {id} { return [expr {[llength $id] == 2 && [lindex $id 0] eq "element" && [string is integer -strict [lindex $id 1]] && ([lindex $id 1] >= 1)}] } # # ## ### ##### ######## ############# ###################### method shape {shape} { set myshape($shape) . return } method isShape {shape} { return [info exists myshape($shape)] } # # ## ### ##### ######## ############# ###################### ## Public API :: Extending the database method new {shape corners items subelements} { # Generate key set id [NewIdentifier] # Save the element information. set myelement($id) [dict create \ shape $shape \ corners $corners \ items $items \ elements $subelements] lappend myhistory() $id lappend myhistory($shape) $id return $id } method drop {} { set mycounter 0 array unset myelement * array unset myhistory * set myhistory() {} return } method {history get} {} { return [array get myhistory] } method {history set} {history} { array unset myhistory * array set myhistory $history return } method {history find} {shape offset} { # 1, 2,...: Offset from the beginning of history, forward. # -1,-2,...: Offset from the end history, backward. if {$offset < 0} { set offset [expr {[llength $myhistory($shape)] + $offset}] } else { incr offset -1 } #parray myhistory #puts E|hf|$shape|$offset| return [lindex $myhistory($shape) $offset] } # # ## ### ##### ######## ############# ###################### ## Public API :: Query database. method elements {} { return $myhistory() } method corner {id corner} { #puts MAP($corner)=|[MapCorner $id $corner]| set corners [dict get $myelement($id) corners] return [dict get $corners [$dir map $corners $corner]] } method corners {id} { return [dict get $myelement($id) corners] } method names {id {pattern *}} { return [dict keys [dict get $myelement($id) corners] $pattern] } method items {args} { set items {} foreach id $args { lappend items {*}[dict get $myelement($id) items] lappend items {*}[$self items {*}[dict get $myelement($id) elements]] } # Elements with sub-elements elements can cause canvas items # to appear multiple times. Reduce this to only one # appearance. Otherwise items may be processed multiple times # later. return [lsort -uniq $items] } method bbox {args} { # We compute the bounding box from the corners we have for the # specified elements. This makes the assumption that the # convex hull of the element's corners is a good approximation # of the areas they cover. # # (1) We cannot fall back to canvas items, as the items may # cover a much smaller area than the system believes. This # notably happens for text elements. In essence a user- # declared WxH would be ignored by looking at the canvas. # # (2) We have to look at all corners because the simple NW/SE # diagonal may underestimate the box. This happens for circles # where these anchors are near the circle boundary and thus # describe the in-scribed box, instead of the outer bounds. # Note that corners may contain other information than # points. This is why the corner values are type tagged, # allowing us to ignore the non-point corners. set polyline {} foreach id $args { foreach v [dict values [dict get $myelement($id) corners]] { lassign $v cmd detail if {$cmd ne "point"} continue lappend polyline [geo::x $detail] [geo::y $detail] } } return [geo::bbox $polyline] } # # ## ### ##### ######## ############# ###################### ## Public API :: Move elements to a point. method relocate {id destination corner canvas} { #puts \trelocate($id).$corner\ @$destination # Move the id'entified element such that the corner's point is # at the destination. # Retrieve element data. array set el $myelement($id) # Find current location of the specified corner. set origin [diagram::point unbox [$self corner $id $corner]] #puts \t$corner=$origin # Determine the movement vector which brings the corner into # coincidence with the destination. set delta [geo::- $destination $origin] #puts \tdelta=$delta # And perform the movement. $self Move $id $delta $canvas return } method move {delta corners} { set newcorners {} foreach {key location} $corners { #puts PLACE|$key|$location|$delta| if {[llength $location] == 2} { lassign $location cmd detail if {$cmd eq "point"} { #puts \tSHIFT lappend newcorners $key \ [list $cmd [geo::+ $detail $delta]] } else { lappend newcorners $key $location } } else { lappend newcorners $key $location } } return $newcorners } method Move {id delta canvas} { # Retrieve element data. array set el $myelement($id) # Move the primary items on the canvas. foreach item $el(items) { $canvas move $item {*}$delta } # Recursively move child elements foreach sid $el(elements) { $self Move $sid $delta $canvas } # And modify the corners appropriately set newcorners [$self move $delta $el(corners)] dict set myelement($id) corners $newcorners return } # # ## ### ##### ######## ############# ###################### constructor {thedir} { set dir $thedir return } # # ## ### ##### ######## ############# ###################### proc NewIdentifier {} { upvar 1 mycounter mycounter return [list element [incr mycounter]] } # # ## ### ##### ######## ############# ###################### ## Instance data, database tables as arrays, keyed by direction ## and alias names. component dir ; # Database of named directions. # Used to check for and resolve # corner aliases. variable mycounter 0 ; # Counter for the generation of # element identifiers. See # 'NewIdentifier' for the user. variable myelement -array {} ; # Database of drawn elements. Maps # from element identifiers to a # dictionary holding the pertinent # information (type, canvas items, # sub elements, and corners (aka # attributes). variable myhistory -array { {} {} } ; # History database. Keyed by # element type, they are mapped to # lists of element identifiers # naming the elements in order of # creation. The empty key has the # history without regard to type. variable myshape -array {} ; # Database of element shapes. ## # # ## ### ##### ######## ############# ###################### } namespace eval ::diagram::element::geo { namespace import ::math::geometry::* } # # ## ### ##### ######## ############# ###################### ## Ready package provide diagram::element 1