## -*- tcl -*- # # ## ### ##### ######## ############# ##################### # Canvas Behavior Module. Editing 2-4 points/vertices describing an axis-aligned rectangle, # i.e. bounding box. # Core interaction behaviour inherited from canvas::edit::points # Configurable: # - Tag used to mark/identify the points of this cloud. # Default: RECTANGLE # # - Callback used to create the item (group) representing the point. # Default: # # - Callback used to report on rectangle editing activity. # Default: NONE. # # - Callback used to report enter/leave events for the rectangle and its points. # Default: NONE. # # ## ### ##### ######## ############# ##################### ## Requisites package require Tcl 8.5- package require Tk package require snit package require canvas::edit::points namespace eval ::canvas::edit { namespace export rectangle namespace ensemble create } # # ## ### ##### ######## ############# ##################### ## API snit::type ::canvas::edit::rectangle { # See canvas::edit::points option -tag -default RECTANGLE -readonly 1 option -create-cmd -default {} \ -configuremethod Chain \ -cgetmethod UnChain # Callback reporting the rectangle after changes (add, remove, drag) option -data-cmd -default {} # Callback reporting when the rectangle or any of the points have the mouse over it option -active-cmd -default {} # See canvas::edit::points, also base config for rectangle option -color -default SkyBlue2 -configuremethod Pass option -hilit-color -default red -configuremethod Pass # See canvas::edit::points option -radius -default 3 -configuremethod Pass option -kind -default oval -configuremethod Pass # See canvas::edit::points, -add-remove also for click on rectangle option -add-remove-point -default {} -readonly 1 option -drag-point -default 3 -readonly 1 # Additional rectangle configuration # NOTE: __Cannot__ supercede -color/-hilit-color option -rect-config -default {} method Pass {o v} { if {$v eq $options($o)} { return 0 } set options($o) $v if {$myeditor eq {}} { return 1 } $myeditor configure $o $v return 1 } method Chain {o v} { if {$v eq $options($o)} { return 0 } set options($o) $v if {$myeditor eq {}} { return 1 } # Reconfigure the editor with our behaviour still in the chain $myeditor configure -create-cmd [mymethod Deny $v] return 1 } method Unchain {o} { # Hide the internal -create-cmd chaining from the user return [$myeditor cget -create-cmd] } # # ## ### ##### ######## ############# ##################### constructor {c args} { set mycanvas $c set mystate {} set myops base $self configurelist $args # Generate an internal point cloud editor, which will handle # the basic tasks regarding the rectangles's vertices. lappend cmd canvas::edit points ${selfns}::P $c lappend cmd -tag $options(-tag) lappend cmd -data-cmd [mymethod Point] lappend cmd -active-cmd [mymethod PointActive] # Pass point options/configuration to the subordinate editor foreach o { -create-cmd -color -hilit-color -radius -kind -add-remove-point -drag-point } { set c $options($o) if {$c ne {}} { lappend cmd $o $c } } set myeditor [{*}$cmd] $myeditor configure -create-cmd \ [mymethod Deny [$myeditor cget -create-cmd]] $mycanvas bind [SegmentTag] [mymethod Active rect] $mycanvas bind [SegmentTag] [mymethod Active {} ] return } component myeditor delegate method enable to myeditor delegate method disable to myeditor delegate method active to myeditor method clear {} { set myops shunt $myeditor clear set myops base set mystate {} set mycoords {} $self Regenerate Note return } method set {minx miny maxx maxy} { $self clear $self Complete $myeditor $minx $miny $maxx $maxy return } # # ## ### ##### ######## ############# ##################### ## Actions bound to events, as reported by the point cloud editor. method Complete {pe minx miny maxx maxy} { set myops shunt # Corners # # tl *--* tr # | | # bl *--* br # Create the proper corner points and remember their associations (id <-> corner) Def $pe tl $minx $miny Def $pe bl $minx $maxy Def $pe tr $maxx $miny Def $pe br $maxx $maxy # enter completion, where the rectangle can be dragged (by its corners), and removed set myops complete $self Regenerate Note return } method Deny {chain c x y} { #puts deny/$myops/$mystate/ # Deny more points when we have the complete set. if {$myops eq "complete"} return # Continue to actual marker creation. return [{*}$chain $c $x $y] } method PointActive {_ corner} { if {$myops ne "complete"} return if {$corner ne {}} { set corner [dict get $mystate $corner] } $self Active $corner return } method Active {kind} { # puts /$kind/ if {![llength $options(-active-cmd)]} return {*}$options(-active-cmd) $self $kind return } method {Point add} {pe id x y} { switch -exact -- $myops { shunt {} base { # Base point arrived, remember, now wait for second corner set mystate [list $id $x $y] set myops partial return } partial { # Second corner has arrived. Complete the rectangle. # Disable point callbacks invoked due to this automatic task. set myops shunt # Get saved first corner lassign $mystate id0 x0 y0 set mystate {} # Compute all corners from it and the current, second, corner set minx [expr { min ($x0, $x) }] set miny [expr { min ($y0, $y) }] set maxx [expr { max ($x0, $x) }] set maxy [expr { max ($y0, $y) }] # Drop old points (base, and current) $pe remove $id $pe remove $id0 # And generate the rectangle $self Complete $pe $minx $miny $maxx $maxy } complete { return -code error "Should have been rejected by `Deny`" } } } method {Point remove} {pe id} { switch -exact -- $myops { shunt {} base { # no points known. nothing to do return } partial { # first point known, no second point. drop memory of first point set mystate {} set myops base return } complete { # removing even one point of the rectangle removes the entire rectangle! # Disable point callbacks invoked due to this automatic task. set myops shunt # Find the corner removed by the user and drop it from the state. # Then remove the remaining corners set corner [dict get $mystate $id] dict unset mystate $id dict unset mystate $corner foreach corner $ourcorners { if {![dict exists $mystate $corner]} continue $pe remove [dict get $mystate $corner] } # enter base state waiting for a new first point set myops base set mystate {} set mycoords {} $self Regenerate Note return } } return } method {Point move start} {pe id} { # Initialize local drag state. set mydid $id set corner [dict get $mystate $id] set mydloc [dict get $mycoords $corner] return } method {Point move delta} {pe id nx ny dx dy} { # Track the movement. set mydloc [list $nx $ny] return } method {Point move done} {pe id} { set corner [dict get $mystate $id] # Get the rectangle data from moving an opposite corner lassign [dict get $mycoords [dict get { tl br tr bl bl tr br tl } $corner]] x1 y1 lassign $mydloc x0 y0 # Update state of the moved point, for proper relative # movement after the coming recalculation Save $id $corner $x0 $y0 # Recompute all corner locations ... set minx [expr { min ($x0, $x1) }] set miny [expr { min ($y0, $y1) }] set maxx [expr { max ($x0, $x1) }] set maxy [expr { max ($y0, $y1) }] # and move the points for the corners to the new locations # One of the points, the current moved may not move again Move $pe tl $minx $miny Move $pe bl $minx $maxy Move $pe tr $maxx $miny Move $pe br $maxx $maxy $self Regenerate Note return 1 } method Regenerate {} { if {$myrect ne {}} { $mycanvas delete $myrect set myrect {} } if {$myops ne "complete"} return lassign [dict get $mycoords tl] minx miny lassign [dict get $mycoords br] maxx maxy set myrect [$mycanvas create rectangle $minx $miny $maxx $maxy \ -fill {} \ -width 2 \ {*}$options(-rect-config) \ -activeoutline $options(-hilit-color) \ -outline $options(-color)] canvas::tag append $mycanvas $myrect [SegmentTag] $mycanvas lower $myrect $options(-tag) return } # # ## ### ##### ######## ############# ##################### ## Corner management proc Move {pe corner nx ny} { upvar 1 mystate mystate mycoords mycoords lassign [dict get $mycoords $corner] ox oy set dx [expr {$nx - $ox}] set dy [expr {$ny - $oy}] set id [dict get $mystate $corner] $pe move-by $id $dx $dy Save $id $corner $nx $ny return } proc Def {pe corner x y} { upvar 1 mystate mystate mycoords mycoords Save [$pe add $x $y] $corner $x $y return } proc Save {id corner x y} { upvar 1 mystate mystate mycoords mycoords dict set mycoords $corner [list $x $y] dict set mystate $corner $id dict set mystate $id $corner return } proc SegmentTag {} { upvar 1 options options return $options(-tag)/Rect } #### Generate notification about changes to the point cloud. proc Note {} { upvar 1 options options self self myops myops mycoords mycoords if {![llength $options(-data-cmd)]} return switch -exact -- $myops { shunt - base - partial { set coords {} } complete { lassign [dict get $mycoords tl] minx miny lassign [dict get $mycoords br] maxx maxy set coords [list $minx $miny $maxx $maxy] } } return [{*}$options(-data-cmd) $self $coords] } # debug support ... proc X {p} { return [lindex [split $p /] 0] } # # ## ### ##### ######## ############# ##################### ## STATE # - Saved handle of the canvas operated on. # - Counter for the generation of point identifiers # - List of the points managed by the object, conveying their # order. # - Canvas items for the actual rectangle typevariable ourcorners {tl tr bl br} variable mycanvas {} ;# The canvas we are working with. variable myeditor {} ;# point editor instance managing the rectangle corners variable mystate {} ;# dict, general state variable myops {} ;# system state controlling callback processing # states # - base No points present, accept base point # - shunt Ignore point editor callback, automatic task in progress # - partial Single point known, wait for the second corner # - complete Rectangle is complete, deny more points variable mycoords {} ;# corner -> pair (x y) variable myrect {} ;# rectangle item variable mydid ; # Drag state. id of the moving point. variable mydloc ; # Drag state. Uncommitted location of the moving point. # # ## ### ##### ######## ############# ##################### } # # ## ### ##### ######## ############# ##################### ## Ready package provide canvas::edit::rectangle 0.1 return # # ## ### ##### ######## ############# ##################### ## Scrap yard.