Zigzag. Question raised comp.lang.tcl <[email protected]> (see e.g. <http://groups.google.com/group/comp.lang.tcl/browse_thread/thread/e590fe10db828f62/6a7df3f81052114b#6a7df3f81052114b >:
Edit a canvas multi-line with mouse clicks. Clicking somewhere near a segment adds a vertex at this point. Clicking and dragging a vertex modifies its coordinates.
This is more intended as a simple example, so I wanted to keep it minimal (thus no deleting).
The idea was to use the canvas data structures as "model".
There are surely many possible improvements: feel free to munge this page accordingly (it's a wiki, after all :-).
I'd be especially interested in style improvements and in whatever makes this snippet shorter and clearer (I'm not a very seasoned Tcler, mind you).
Screenshot:
#!/usr/bin/wish8.5
# This code is in the public domain. Use, enjoy.
pack [canvas .c]
# Draw a decoration on a vertex
# All of them have the tag "deco"
proc dot {x y} {
.c create polygon [expr {$x + 3}] $y $x [expr {$y - 3}] \
[expr {$x - 3}] $y $x [expr {$y + 3}] \
-fill orange -outline blue -tags deco
}
# Find point on segment (p--q) nearest to (xp,yp).
# Return (xxp yyp, d) where (xxp, yyp) are the coords of this
# nearest point and d is the distance to the segment.
proc prox-seg {px py qx qy rx ry} {
set ux [expr { $qx - $px }]
set uy [expr { $qy - $py }]
set vx [expr { $rx - $px }]
set vy [expr { $ry - $py }]
set u [expr { hypot($ux, $uy) }]
set v [expr { hypot($vx, $vy) }]
# c is the relative position of this point on the segment
# (0 --> p, 1 --> q, i.e. c < 0 or c > 1 means point is
# outisde the segment)
set c [expr {($ux * $vx + $uy * $vy) / ($u * $u)}]
# if we clamp it to [0,1] we'll never lie outside the segment:
set c [expr {$c<0 ? 0 : $c>1 ? 1 : $c}]
set wx [expr { $ux * $c }]
set wy [expr { $uy * $c }]
set d [expr { hypot($vx - $wx, $vy - $wy) }]
return [list [expr {$px + $wx}] [expr {$py + $wy}] $c $d]
}
# Find the point on (multi-)line nearest to given x y
# Return coords of point, segment number (0..n-1), relative pos
# whithin that segment and distance to that segment
# Note that this gets inaccurate when the nearest point is near
# the beginning or end of the line: we don't mind, because we
# just trigger "near" the line
proc prox-line {line x y} {
set nseg -1
foreach {qx qy} [.c coords $line] {
if {[info locals px] != ""} { # else we are first time here
# Current segment is p--q
set thisseg [prox-seg $px $py $qx $qy $x $y]
set dist [lindex $thisseg 3]
if { [info locals mindist] == "" ||
( $dist < $mindist && [lindex $thisseg 2] >=0 && [lindex $thisseg 2] <=1 ) } {
set minseg $thisseg
set mindist $dist
set minnseg $nseg
}
}
set px $qx
set py $qy
incr nseg
}
return [list [lindex $minseg 0] [lindex $minseg 1] $minnseg [lindex $minseg 2] [lindex $minseg 3]]
}
# return no [0..n] of line's vertex next to x y
proc findvertex {line x y} {
set n 0
foreach {vx vy} [.c coords $line] {
set d [expr {hypot($x - $vx, $y - $vy)}]
if {[info locals dmin] == "" || $d < $dmin} {
set dmin $d
set nmin $n
}
incr n
}
return $nmin
}
# Add a vertex to zigzag nearest to x y
proc zig {wx wy} {
# NOTE receives window coords -- thanks [MLai]!
set x [.c canvasx $wx]
set y [.c canvasy $wy]
global zigzag
set pos [prox-line $zigzag $x $y]
.c insert $zigzag [expr {2 * (1 + [lindex $pos 2])}] [list $x $y]
dot $x $y
drag-start $x $y
}
# Dragging vertices (ripped off Tk demo). Three procs manage the dragging
proc drag-start {wx wy} {
# NOTE receives window coords -- thanks [MLai]!
set x [.c canvasx $wx]
set y [.c canvasy $wy]
global lastx lasty
puts [.c find withtag deco]
# Find out which decoration item(s) to drag
set deco {}
foreach it [.c find closest $x $y] {
puts "$it: [.c gettags $it]"
if {[lsearch -all -exact -inline [.c gettags $it] deco] ne ""} {lappend deco $it}
}
puts $deco
bind .c <B1-Motion> [list dragging $deco %x %y]
bind .c <ButtonRelease-1> [list drag-end %x %y]
set lastx $x
set lasty $y
}
proc dragging {it x y} {
global lastx lasty zigzag
.c move $it [expr $x - $lastx] [expr $y - $lasty]
set v [expr {2 * [findvertex $zigzag $lastx $lasty]}]
# insert before v, delete after newly inserted:
.c insert $zigzag $v [list $x $y]
.c dchars $zigzag [expr {$v + 2}] [expr {$v + 3}]
set lastx $x
set lasty $y
}
proc drag-end {x y} {
bind .c <ButtonRelease-1> {}
bind .c <B1-Motion> {}
}
# Draw initial line, decorate, set up bindings
set zigzag [.c create line 10 10 60 250 300 150 -fill blue]
foreach {x y} [.c coords $zigzag] {dot $x $y}
.c bind $zigzag <ButtonPress-1> [list zig %x %y]
.c bind deco <ButtonPress-1> [list drag-start %x %y]See also: Drawing and editing polygons
MLai 2013-10-10
Very useful! I adapt your code in my program and then notice sometimes the dot (polygon) is not right on the line. I add the following lines to your routines to make this "offset" problem go away:
set x [.c canvasx $x] set y [.c canvasy $y]
Not sure if the offset problem is something specific in my application.
tomas 2013-12-25
Thanks for the good catch, MLai!
I corrected it in the code: both zig and drag-start receive mouse coordinates, which are relative to the canvas's viewport origin and not to the canvas's origin: when they are not the same (e.g. when the canvas is scrolled, but perhaps on some platforms), bad things happen.