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
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.
|
|
|