7

I am trying to use turtle draw with mouse, I got below demo code works but cursor jump sometimes during mouse move:

#!/usr/bin/env python
import turtle
import sys

width = 600
height = 300
def gothere(event):
    turtle.penup()
    x = event.x
    y = event.y
    print "gothere (%d,%d)"%(x,y)
    turtle.goto(x,y)
    turtle.pendown()

def movearound(event):
    x = event.x
    y = event.y
    print "movearound (%d,%d)"%(x,y)
    turtle.goto(x,y)

def release(event):
    print "release"
    turtle.penup()

def circle(x,y,r):
    turtle.pendown() 
    turtle.goto(x,y)
    turtle.circle(r)
    turtle.penup()
    return

def reset(event):
    print "reset"
    turtle.clear()

#------------------------------------------------#
sys.setrecursionlimit(90000)
turtle.screensize(canvwidth=width, canvheight=height, bg=None)
turtle.reset()
turtle.speed(0)
turtle.setup(width, height)

canvas = turtle.getcanvas()

canvas.bind("<Button-1>", gothere)
canvas.bind("<B1-Motion>", movearound)
canvas.bind("<ButtonRelease-1>", release)
canvas.bind("<Escape>",reset)

screen = turtle.Screen()
screen.setworldcoordinates(0,height,width,0)
screen.listen()

turtle.mainloop()
#------------------------------------------------#

see below gif for actual behavior:

enter image description here

Not sure if any API call is wrong!

Kevin
  • 74,910
  • 12
  • 133
  • 166
lucky1928
  • 8,708
  • 10
  • 43
  • 92
  • 3
    `sys.setrecursionlimit(90000)` is suspicious to me. If you added that because you were getting an unusual error with a very long stack trace before, I suspect that error has something to do with the problem you're having now. – Kevin Apr 30 '18 at 18:12
  • Looks like it happens whenever you move the mouse before the turtle has arrived at the cursor position. When you click, you can see the turtle move toward the cursor - if you start moving the mouse before it's arrived at the cursor, it won't draw a line while you're dragging and once you release the mouse button, you'll get that weird jumping behavior. – Aran-Fey Apr 30 '18 at 18:28
  • 2
    If I remove the `setrecursionlimit` line, then this code occasionally produces a traceback that repeatedly enters `movearound`. I suspect this is happening because `movearound` calls `goto`, which calls `update`, which checks for mouse updates and may call `movearound` again. If Tkinter does not always evaluate mouse events in the order it received them, this might explain the jittery movement of the turtle. http://effbot.org/tkinterbook/widget.htm says that using `update()` inside a callback can lead to "nasty race conditions", which appears to be exactly what's happening here. – Kevin Apr 30 '18 at 18:43
  • (Posting the above comment as a comment and not an answer because I don't have a quick fix that would solve the problem; the best thing to do is simply to not call `goto()` inside a function bound to the canvas. But if he doesn't do that, how would the OP achieve his desired behavior?) – Kevin Apr 30 '18 at 18:45
  • Adding `self.tracer(2,0)` seems to solve your issue! – TwistedSim Apr 30 '18 at 19:40

3 Answers3

5

I see several issues with your code:

  • You're mixing the object-oriented interface to turtle with the functional interface to that module. I recommend one or the other, but not both. See my import change to force OOP-only.

  • You're using low level tkinter mouse and key events instead of turtle's own events. I recommend you try to work at the turtle level (though this introduces a glitch compared to your implementation, see below.)

  • You're introducing unintended recursion by not turning off events inside your event handlers. Disabling events inside those handlers that take significant time will clean up your graphics.

Here's my rework of your code along the above lines. The one glitch is that unlike your original, the "move turtle here" and "start dragging" will take two clicks, one screen click to move the turtle to the current location and one turtle click to start dragging. This is due to differences in the interface that turtle provides to tkinter events. (Python 3 is a little better in this regard but not for this situation.)

To ease that, I used a larger turtle cursor. I also added heading logic:

from turtle import Turtle, Screen, mainloop

WIDTH = 600
HEIGHT = 300

def gothere(x, y):
    screen.onscreenclick(gothere)  # disable events inside handler

    turtle.penup()
    print("gothere (%d,%d)" % (x, y))
    turtle.goto(x, y)
    turtle.pendown()

    screen.onscreenclick(gothere)

def movearound(x, y):
    turtle.ondrag(None)  # disable events inside handler

    turtle.setheading(turtle.towards(x, y))
    print("movearound (%d,%d)" % (x, y))
    turtle.goto(x, y)

    turtle.ondrag(movearound)

def release(x, y):
    print("release (%d,%d)" % (x, y))
    turtle.penup()

def reset():
    print("reset")
    turtle.clear()

screen = Screen()
screen.setup(WIDTH, HEIGHT)
# screen.setworldcoordinates(0, HEIGHT, WIDTH, 0)  # should work fine either way

turtle = Turtle('turtle')
turtle.speed('fastest')

turtle.ondrag(movearound)
turtle.onrelease(release)

screen.onscreenclick(gothere)
screen.onkey(reset, "Escape")

screen.listen()

mainloop()  # normally screen.mainloop() but not in Python 2

But also see this answer where I show how to make tkinter's onmove event available to turtle.

... the "move here" then "start drag" limitation is very not comfortable for user? How can we improve that?

Combining my above code with my alternate answer that I linked to, we get this solution that is similar to where you started but without the glitches and in a more turtle-like style:

from turtle import Turtle, Screen, mainloop
from functools import partial

WIDTH = 600
HEIGHT = 300

VERBOSE = False

def onscreenmove(self, fun, btn=1, add=None):  # method missing from turtle.py

    if fun is None:
        self.cv.unbind('<Button%s-Motion>' % btn)
    else:
        def eventfun(event):
            fun(self.cv.canvasx(event.x) / self.xscale, -self.cv.canvasy(event.y) / self.yscale)

        self.cv.bind('<Button%s-Motion>' % btn, eventfun, add)

def onscreenrelease(self, fun, btn=1, add=None):  # method missing from turtle.py

    if fun is None:
        self.cv.unbind("<Button%s-ButtonRelease>" % btn)
    else:
        def eventfun(event):
            fun(self.cv.canvasx(event.x) / self.xscale, -self.cv.canvasy(event.y) / self.yscale)

        self.cv.bind("<Button%s-ButtonRelease>" % btn, eventfun, add)

def gothere(x, y):

    if VERBOSE:
        print("gothere (%d,%d)" % (x, y))

    turtle.penup()
    turtle.goto(x, y)
    turtle.pendown()

def movearound(x, y):

    screen.onscreenmove(None)  # disable events inside handler

    if VERBOSE:
        print("movearound (%d,%d)" % (x, y))


    turtle.setheading(turtle.towards(x, y))
    turtle.goto(x, y)

    screen.onscreenmove(movearound)  # reenable events

def release(x, y):

    if VERBOSE:
        print("release (%d,%d)" % (x, y))

    turtle.penup()

def reset():

    if VERBOSE:
        print("reset")

    turtle.clear()

screen = Screen()
screen.setup(WIDTH, HEIGHT)
screen.onscreenrelease = partial(onscreenrelease, screen)  # install missing methods
screen.onscreenmove = partial(onscreenmove, screen)

turtle = Turtle('turtle')
turtle.speed('fastest')

screen.onscreenclick(gothere)
screen.onscreenrelease(release)
screen.onscreenmove(movearound)

screen.onkey(reset, "Escape")
screen.listen()

mainloop()  # normally screen.mainloop() but not in Python 2
cdlane
  • 40,441
  • 5
  • 32
  • 81
  • Great but the "move here" then "start drag" limitation is very not comfortable for user? How can we improve that? – lucky1928 May 01 '18 at 16:41
  • @lucky1928, I augmented my answer with a rework of my code that adds back some missing tkinter events to turtle to get the interaction style you desire. – cdlane May 01 '18 at 18:39
4

I agree with cdlane that it would be better to avoid Tkinter-level event binding where possible, but I thought it might also be interesting to approach the problem without altering the overall design much. My solution only requires an additional import:

from collections import deque

And a new version of movearound:

pending = deque()
def movearound(event):
    x = event.x
    y = event.y
    pending.append((x,y))
    if len(pending) == 1:
        while pending:
            x,y = pending[0]
            turtle.goto(x,y)
            pending.popleft()

As I indicated in my comments, goto can call movearound if the window's event queue is backed up, and this can cause a stack overflow or race conditions that make the turtle move in unusual ways. This approach aims to prevent arbitrarily deep recursion, by only letting the topmost instance of movearound call goto. if len(pending) == 1: should only succeed for nonrecursive calls; all recursive calls will see a queue larger than that. The while pending: loop works through all the built-up events, handling them in the order they arrived.

The result: a turtle that dutifully follows the path of the cursor, albeit at its own turtley pace:

enter image description here

Kevin
  • 74,910
  • 12
  • 133
  • 166
0

Adding turtle.tracer(2,0) seems to make the issue disappear. This may be a temporary solution since I don't know if it just hide the problem behind another one.

...
screen = turtle.Screen()
screen.setworldcoordinates(0,height,width,0)
screen.listen()
turtle.tracer(2, 0)  # This line

turtle.mainloop()
...

In the documentation we can read:

Turns turtle animation on/off and set delay for update drawings.

As @Keven points out, the issue seems to be the implicit update in the callback. This reduce the number of update call by 2. I think it may also remove the recursive call.

Note: I don't know why but removing screen.setworldcoordinates(0,height,width,0) removes the glitchy behavior, but the recursive call to update is still present.

TwistedSim
  • 1,960
  • 9
  • 23