You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

452 lines
11 KiB

## -*- 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: <Inherited from the subordinate point cloud editor>
#
# - 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] <Enter> [mymethod Active rect]
$mycanvas bind [SegmentTag] <Leave> [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.