mirror of
https://github.com/python/cpython.git
synced 2026-10-28 18:35:21 +08:00
Co-authored-by: Maciej Olko <maciej.olko@affirm.com> Co-authored-by: Carol Willing <carolcode@willingconsulting.com>
4260 lines
145 KiB
Python
4260 lines
145 KiB
Python
#
|
|
# turtle.py: a Tkinter based turtle graphics module for Python
|
|
# Version 1.1b - 4. 5. 2009
|
|
#
|
|
# Copyright (C) 2006 - 2010 Gregor Lingl
|
|
# email: glingl@aon.at
|
|
#
|
|
# This software is provided 'as-is', without any express or implied
|
|
# warranty. In no event will the authors be held liable for any damages
|
|
# arising from the use of this software.
|
|
#
|
|
# Permission is granted to anyone to use this software for any purpose,
|
|
# including commercial applications, and to alter it and redistribute it
|
|
# freely, subject to the following restrictions:
|
|
#
|
|
# 1. The origin of this software must not be misrepresented; you must not
|
|
# claim that you wrote the original software. If you use this software
|
|
# in a product, an acknowledgment in the product documentation would be
|
|
# appreciated but is not required.
|
|
# 2. Altered source versions must be plainly marked as such, and must not be
|
|
# misrepresented as being the original software.
|
|
# 3. This notice may not be removed or altered from any source distribution.
|
|
|
|
"""
|
|
Imagine a robotic turtle starting at (0, 0) in the x-y plane.
|
|
After an `import turtle`, give it the command `turtle.forward(15)`, and it
|
|
moves (on-screen!) 15 pixels in the direction it is facing, drawing a line as
|
|
it moves. Give it the command `turtle.right(25)`, and it rotates in-place 25
|
|
degrees clockwise.
|
|
|
|
Turtle graphics is an implementation of the drawing tools introduced in Logo
|
|
in 1967. It was created as an educational tool, and its instant, visible
|
|
feedback makes it an effective way for learners to encounter programming
|
|
concepts. It is also a convenient way to produce simple graphical output
|
|
without bringing in external libraries.
|
|
"""
|
|
|
|
import tkinter as TK
|
|
import types
|
|
import math
|
|
import time
|
|
import inspect
|
|
import sys
|
|
|
|
from os import environ
|
|
from os.path import isfile, split, join
|
|
from pathlib import Path
|
|
from contextlib import contextmanager
|
|
from copy import deepcopy
|
|
from tkinter import simpledialog
|
|
|
|
_tg_classes = ['ScrolledCanvas', 'TurtleScreen', 'Screen',
|
|
'RawTurtle', 'Turtle', 'RawPen', 'Pen', 'Shape', 'Vec2D']
|
|
_tg_screen_functions = ['addshape', 'bgcolor', 'bgpic', 'bye',
|
|
'clearscreen', 'colormode', 'delay', 'exitonclick', 'getcanvas',
|
|
'getshapes', 'listen', 'mainloop', 'mode', 'no_animation', 'numinput',
|
|
'onkey', 'onkeypress', 'onkeyrelease', 'onscreenclick', 'ontimer',
|
|
'register_shape', 'resetscreen', 'screensize', 'save', 'setup',
|
|
'setworldcoordinates', 'textinput', 'title', 'tracer', 'turtles',
|
|
'update', 'window_height', 'window_width']
|
|
_tg_turtle_functions = ['back', 'backward', 'begin_fill', 'begin_poly', 'bk',
|
|
'circle', 'clear', 'clearstamp', 'clearstamps', 'clone', 'color',
|
|
'degrees', 'distance', 'dot', 'down', 'end_fill', 'end_poly', 'fd',
|
|
'fillcolor', 'fill', 'filling', 'forward', 'get_poly', 'getpen',
|
|
'getscreen', 'get_shapepoly', 'getturtle', 'goto', 'heading',
|
|
'hideturtle', 'home', 'ht', 'isdown', 'isvisible', 'left', 'lt',
|
|
'onclick', 'ondrag', 'onrelease', 'pd', 'pen', 'pencolor', 'pendown',
|
|
'pensize', 'penup', 'poly', 'pos', 'position', 'pu', 'radians', 'right',
|
|
'reset', 'resizemode', 'rt', 'seth', 'setheading', 'setpos',
|
|
'setposition', 'setundobuffer', 'setx', 'sety', 'shape', 'shapesize',
|
|
'shapetransform', 'shearfactor', 'showturtle', 'speed', 'st', 'stamp',
|
|
'teleport', 'tilt', 'tiltangle', 'towards', 'turtlesize', 'undo',
|
|
'undobufferentries', 'up', 'width',
|
|
'write', 'xcor', 'ycor']
|
|
_tg_utilities = ['write_docstringdict', 'done']
|
|
|
|
__all__ = (_tg_classes + _tg_screen_functions + _tg_turtle_functions +
|
|
_tg_utilities + ['Terminator'])
|
|
|
|
_alias_list = ['addshape', 'backward', 'bk', 'fd', 'ht', 'lt', 'pd', 'pos',
|
|
'pu', 'rt', 'seth', 'setpos', 'setposition', 'st',
|
|
'turtlesize', 'up', 'width']
|
|
|
|
_CFG = {"width" : 0.5, # Screen
|
|
"height" : 0.75,
|
|
"canvwidth" : 400,
|
|
"canvheight": 300,
|
|
"leftright": None,
|
|
"topbottom": None,
|
|
"mode": "standard", # TurtleScreen
|
|
"colormode": 1.0,
|
|
"delay": 10,
|
|
"undobuffersize": 1000, # RawTurtle
|
|
"shape": "classic",
|
|
"pencolor" : "black",
|
|
"fillcolor" : "black",
|
|
"resizemode" : "noresize",
|
|
"visible" : True,
|
|
"language": "english", # docstrings
|
|
"exampleturtle": "turtle",
|
|
"examplescreen": "screen",
|
|
"title": "Python Turtle Graphics",
|
|
"using_IDLE": False
|
|
}
|
|
|
|
def config_dict(filename):
|
|
"""Convert the content of a config file into a dict."""
|
|
with open(filename, "r") as f:
|
|
cfglines = f.readlines()
|
|
cfgdict = {}
|
|
for line in cfglines:
|
|
line = line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
try:
|
|
key, value = line.split("=")
|
|
except ValueError:
|
|
print("Bad line in config-file %s:\n%s" % (filename,line))
|
|
continue
|
|
key = key.strip()
|
|
value = value.strip()
|
|
if value in ["True", "False", "None", "''", '""']:
|
|
value = eval(value)
|
|
else:
|
|
try:
|
|
if "." in value:
|
|
value = float(value)
|
|
else:
|
|
value = int(value)
|
|
except ValueError:
|
|
pass # value need not be converted
|
|
cfgdict[key] = value
|
|
return cfgdict
|
|
|
|
def readconfig(cfgdict):
|
|
"""Read config files and update the configuration dict accordingly.
|
|
|
|
If there is a turtle.cfg file in the current working directory,
|
|
read it. If it contains an importconfig value, say 'myway',
|
|
use the filename turtle_myway.cfg, otherwise turtle.cfg, and
|
|
read that file from the directory where turtle.py is located.
|
|
|
|
The configuration dict is updated first from the config file in
|
|
the turtle.py directory, then from the one in the current working
|
|
directory. If no config file is found, the default configuration
|
|
is used.
|
|
"""
|
|
default_cfg = "turtle.cfg"
|
|
cfgdict1 = {}
|
|
cfgdict2 = {}
|
|
if isfile(default_cfg):
|
|
cfgdict1 = config_dict(default_cfg)
|
|
if "importconfig" in cfgdict1:
|
|
default_cfg = "turtle_%s.cfg" % cfgdict1["importconfig"]
|
|
try:
|
|
head, tail = split(__file__)
|
|
cfg_file2 = join(head, default_cfg)
|
|
except Exception:
|
|
cfg_file2 = ""
|
|
if isfile(cfg_file2):
|
|
cfgdict2 = config_dict(cfg_file2)
|
|
_CFG.update(cfgdict2)
|
|
_CFG.update(cfgdict1)
|
|
|
|
try:
|
|
readconfig(_CFG)
|
|
except Exception:
|
|
print ("No configfile read, reason unknown")
|
|
|
|
|
|
class Vec2D(tuple):
|
|
"""A two-dimensional vector class, used as a helper class for
|
|
implementing turtle graphics. May be useful for turtle graphics
|
|
programs too. Derived from tuple, so a vector is a tuple!
|
|
|
|
Provides (for a, b vectors, k number):
|
|
a + b: vector addition
|
|
a - b: vector subtraction
|
|
a * b: inner product
|
|
k * a and a * k: multiplication with scalar
|
|
abs(a): absolute value of a
|
|
a.rotate(angle): rotation
|
|
"""
|
|
|
|
def __new__(cls, x, y):
|
|
return tuple.__new__(cls, (x, y))
|
|
|
|
def __add__(self, other):
|
|
return Vec2D(self[0]+other[0], self[1]+other[1])
|
|
|
|
def __mul__(self, other):
|
|
if isinstance(other, Vec2D):
|
|
return self[0]*other[0]+self[1]*other[1]
|
|
return Vec2D(self[0]*other, self[1]*other)
|
|
|
|
def __rmul__(self, other):
|
|
if isinstance(other, int) or isinstance(other, float):
|
|
return Vec2D(self[0]*other, self[1]*other)
|
|
return NotImplemented
|
|
|
|
def __sub__(self, other):
|
|
return Vec2D(self[0]-other[0], self[1]-other[1])
|
|
|
|
def __neg__(self):
|
|
return Vec2D(-self[0], -self[1])
|
|
|
|
def __abs__(self):
|
|
return math.hypot(*self)
|
|
|
|
def rotate(self, angle):
|
|
"""Rotate self counterclockwise by angle."""
|
|
perp = Vec2D(-self[1], self[0])
|
|
angle = math.radians(angle)
|
|
c, s = math.cos(angle), math.sin(angle)
|
|
return Vec2D(self[0]*c+perp[0]*s, self[1]*c+perp[1]*s)
|
|
|
|
def __getnewargs__(self):
|
|
return (self[0], self[1])
|
|
|
|
def __repr__(self):
|
|
return "(%.2f,%.2f)" % self
|
|
|
|
|
|
##############################################################################
|
|
### From here up to line : Tkinter - Interface for turtle.py ###
|
|
### May be replaced by an interface to some different graphics toolkit ###
|
|
##############################################################################
|
|
|
|
## helper functions for Scrolled Canvas, to forward Canvas-methods
|
|
## to ScrolledCanvas class
|
|
|
|
def __methodDict(cls, _dict):
|
|
"""Helper function for Scrolled Canvas."""
|
|
baseList = list(cls.__bases__)
|
|
baseList.reverse()
|
|
for _super in baseList:
|
|
__methodDict(_super, _dict)
|
|
for key, value in cls.__dict__.items():
|
|
if type(value) == types.FunctionType:
|
|
_dict[key] = value
|
|
|
|
def __methods(cls):
|
|
"""Helper function for Scrolled Canvas."""
|
|
_dict = {}
|
|
__methodDict(cls, _dict)
|
|
return _dict.keys()
|
|
|
|
__stringBody = (
|
|
'def %(method)s(self, *args, **kw): return ' +
|
|
'self.%(attribute)s.%(method)s(*args, **kw)')
|
|
|
|
def __forwardmethods(fromClass, toClass, toPart, exclude = ()):
|
|
### MANY CHANGES ###
|
|
_dict_1 = {}
|
|
__methodDict(toClass, _dict_1)
|
|
_dict = {}
|
|
mfc = __methods(fromClass)
|
|
for ex in _dict_1.keys():
|
|
if ex[:1] == '_' or ex[-1:] == '_' or ex in exclude or ex in mfc:
|
|
pass
|
|
else:
|
|
_dict[ex] = _dict_1[ex]
|
|
|
|
for method, func in _dict.items():
|
|
d = {'method': method, 'func': func}
|
|
if isinstance(toPart, str):
|
|
execString = \
|
|
__stringBody % {'method' : method, 'attribute' : toPart}
|
|
exec(execString, d)
|
|
setattr(fromClass, method, d[method]) ### NEWU!
|
|
|
|
|
|
class ScrolledCanvas(TK.Frame):
|
|
"""Modeled after the scrolled canvas class from Grayons's Tkinter book.
|
|
|
|
Used as the default canvas, which pops up automatically when
|
|
using turtle graphics functions or the Turtle class.
|
|
"""
|
|
def __init__(self, master, width=500, height=350,
|
|
canvwidth=600, canvheight=500):
|
|
TK.Frame.__init__(self, master, width=width, height=height)
|
|
self._rootwindow = self.winfo_toplevel()
|
|
self.width, self.height = width, height
|
|
self.canvwidth, self.canvheight = canvwidth, canvheight
|
|
self.bg = "white"
|
|
self._canvas = TK.Canvas(master, width=width, height=height,
|
|
bg=self.bg, relief=TK.SUNKEN, borderwidth=2)
|
|
self.hscroll = TK.Scrollbar(master, command=self._canvas.xview,
|
|
orient=TK.HORIZONTAL)
|
|
self.vscroll = TK.Scrollbar(master, command=self._canvas.yview)
|
|
self._canvas.configure(xscrollcommand=self.hscroll.set,
|
|
yscrollcommand=self.vscroll.set)
|
|
self.rowconfigure(0, weight=1, minsize=0)
|
|
self.columnconfigure(0, weight=1, minsize=0)
|
|
self._canvas.grid(padx=1, in_ = self, pady=1, row=0,
|
|
column=0, rowspan=1, columnspan=1, sticky='news')
|
|
self.vscroll.grid(padx=1, in_ = self, pady=1, row=0,
|
|
column=1, rowspan=1, columnspan=1, sticky='news')
|
|
self.hscroll.grid(padx=1, in_ = self, pady=1, row=1,
|
|
column=0, rowspan=1, columnspan=1, sticky='news')
|
|
self.reset()
|
|
self._rootwindow.bind('<Configure>', self.onResize)
|
|
|
|
def reset(self, canvwidth=None, canvheight=None, bg = None):
|
|
"""Adjust canvas size, background color and scrollbars."""
|
|
if canvwidth:
|
|
self.canvwidth = canvwidth
|
|
if canvheight:
|
|
self.canvheight = canvheight
|
|
if bg:
|
|
self.bg = bg
|
|
self._canvas.config(bg=bg,
|
|
scrollregion=(-self.canvwidth//2, -self.canvheight//2,
|
|
self.canvwidth//2, self.canvheight//2))
|
|
self._canvas.xview_moveto(0.5*(self.canvwidth - self.width + 30) /
|
|
self.canvwidth)
|
|
self._canvas.yview_moveto(0.5*(self.canvheight- self.height + 30) /
|
|
self.canvheight)
|
|
self.adjustScrolls()
|
|
|
|
|
|
def adjustScrolls(self):
|
|
"""Adjust scrollbars according to window and canvas size."""
|
|
cwidth = self._canvas.winfo_width()
|
|
cheight = self._canvas.winfo_height()
|
|
self._canvas.xview_moveto(0.5*(self.canvwidth-cwidth)/self.canvwidth)
|
|
self._canvas.yview_moveto(0.5*(self.canvheight-cheight)/self.canvheight)
|
|
if cwidth < self.canvwidth or cheight < self.canvheight:
|
|
self.hscroll.grid(padx=1, in_ = self, pady=1, row=1,
|
|
column=0, rowspan=1, columnspan=1, sticky='news')
|
|
self.vscroll.grid(padx=1, in_ = self, pady=1, row=0,
|
|
column=1, rowspan=1, columnspan=1, sticky='news')
|
|
else:
|
|
self.hscroll.grid_forget()
|
|
self.vscroll.grid_forget()
|
|
|
|
def onResize(self, event):
|
|
"""self-explanatory"""
|
|
self.adjustScrolls()
|
|
|
|
def bbox(self, *args):
|
|
""" 'forward' method, which canvas itself has inherited...
|
|
"""
|
|
return self._canvas.bbox(*args)
|
|
|
|
def cget(self, *args, **kwargs):
|
|
""" 'forward' method, which canvas itself has inherited...
|
|
"""
|
|
return self._canvas.cget(*args, **kwargs)
|
|
|
|
def config(self, *args, **kwargs):
|
|
""" 'forward' method, which canvas itself has inherited...
|
|
"""
|
|
self._canvas.config(*args, **kwargs)
|
|
|
|
def bind(self, *args, **kwargs):
|
|
""" 'forward' method, which canvas itself has inherited...
|
|
"""
|
|
self._canvas.bind(*args, **kwargs)
|
|
|
|
def unbind(self, *args, **kwargs):
|
|
""" 'forward' method, which canvas itself has inherited...
|
|
"""
|
|
self._canvas.unbind(*args, **kwargs)
|
|
|
|
def focus_force(self):
|
|
""" 'forward' method, which canvas itself has inherited...
|
|
"""
|
|
self._canvas.focus_force()
|
|
|
|
__forwardmethods(ScrolledCanvas, TK.Canvas, '_canvas')
|
|
|
|
|
|
class _Root(TK.Tk):
|
|
"""Root class for Screen based on Tkinter."""
|
|
def __init__(self):
|
|
TK.Tk.__init__(self)
|
|
|
|
def setupcanvas(self, width, height, cwidth, cheight):
|
|
self._canvas = ScrolledCanvas(self, width, height, cwidth, cheight)
|
|
self._canvas.pack(expand=1, fill="both")
|
|
|
|
def _getcanvas(self):
|
|
return self._canvas
|
|
|
|
def set_geometry(self, width, height, startx, starty):
|
|
self.geometry("%dx%d%+d%+d"%(width, height, startx, starty))
|
|
|
|
def ondestroy(self, destroy):
|
|
self.wm_protocol("WM_DELETE_WINDOW", destroy)
|
|
|
|
def win_width(self):
|
|
return self.winfo_screenwidth()
|
|
|
|
def win_height(self):
|
|
return self.winfo_screenheight()
|
|
|
|
Canvas = TK.Canvas
|
|
|
|
|
|
class TurtleScreenBase:
|
|
"""Provide the basic graphics functionality.
|
|
Interface between Tkinter and turtle.py.
|
|
|
|
To port turtle.py to some different graphics toolkit
|
|
a corresponding TurtleScreenBase class has to be implemented.
|
|
"""
|
|
|
|
def _blankimage(self):
|
|
"""Return a blank image object."""
|
|
img = TK.PhotoImage(width=1, height=1, master=self.cv)
|
|
img.blank()
|
|
return img
|
|
|
|
def _image(self, filename):
|
|
"""Return an image object containing the
|
|
imagedata from an image file named filename.
|
|
"""
|
|
return TK.PhotoImage(file=filename, master=self.cv)
|
|
|
|
def __init__(self, cv):
|
|
self.cv = cv
|
|
if isinstance(cv, ScrolledCanvas):
|
|
w = self.cv.canvwidth
|
|
h = self.cv.canvheight
|
|
else: # expected: ordinary TK.Canvas
|
|
w = int(self.cv.cget("width"))
|
|
h = int(self.cv.cget("height"))
|
|
self.cv.config(scrollregion = (-w//2, -h//2, w//2, h//2 ))
|
|
self.canvwidth = w
|
|
self.canvheight = h
|
|
self.xscale = self.yscale = 1.0
|
|
self._updating = False
|
|
|
|
def _createpoly(self):
|
|
"""Create an invisible polygon item on canvas self.cv."""
|
|
return self.cv.create_polygon((0, 0, 0, 0, 0, 0), fill="", outline="")
|
|
|
|
def _drawpoly(self, polyitem, coordlist, fill=None,
|
|
outline=None, width=None, top=False):
|
|
"""Configure polygon item polyitem according to the given arguments.
|
|
|
|
Arguments:
|
|
coordlist -- a sequence of coordinates
|
|
fill -- the fill color
|
|
outline -- the outline color
|
|
width -- the outline width
|
|
top -- a boolean value, which specifies if polyitem will be
|
|
put on top of the canvas' display list so it will not
|
|
be covered by other items
|
|
"""
|
|
cl = []
|
|
for x, y in coordlist:
|
|
cl.append(x * self.xscale)
|
|
cl.append(-y * self.yscale)
|
|
self.cv.coords(polyitem, *cl)
|
|
if fill is not None:
|
|
self.cv.itemconfigure(polyitem, fill=fill)
|
|
if outline is not None:
|
|
self.cv.itemconfigure(polyitem, outline=outline)
|
|
if width is not None:
|
|
self.cv.itemconfigure(polyitem, width=width)
|
|
if top:
|
|
self.cv.tag_raise(polyitem)
|
|
|
|
def _createline(self):
|
|
"""Create an invisible line item on canvas self.cv."""
|
|
return self.cv.create_line(0, 0, 0, 0, fill="", width=2,
|
|
capstyle = TK.ROUND)
|
|
|
|
def _drawline(self, lineitem, coordlist=None,
|
|
fill=None, width=None, top=False):
|
|
"""Configure line item lineitem according to the given arguments.
|
|
|
|
Arguments:
|
|
coordlist -- a sequence of coordinates
|
|
fill -- the drawing color
|
|
width -- the width of the drawn line
|
|
top -- a boolean value, which specifies if lineitem will be
|
|
put on top of the canvas' display list so it will not
|
|
be covered by other items
|
|
"""
|
|
if coordlist is not None:
|
|
cl = []
|
|
for x, y in coordlist:
|
|
cl.append(x * self.xscale)
|
|
cl.append(-y * self.yscale)
|
|
self.cv.coords(lineitem, *cl)
|
|
if fill is not None:
|
|
self.cv.itemconfigure(lineitem, fill=fill)
|
|
if width is not None:
|
|
self.cv.itemconfigure(lineitem, width=width)
|
|
if top:
|
|
self.cv.tag_raise(lineitem)
|
|
|
|
def _delete(self, item):
|
|
"""Delete graphics item from canvas.
|
|
|
|
If item is "all", delete all graphics items.
|
|
"""
|
|
self.cv.delete(item)
|
|
|
|
def _update(self):
|
|
"""Redraw graphics items on canvas."""
|
|
if self._updating:
|
|
# Reentrant call (e.g. a drag handler moving the turtle,
|
|
# gh-50966): flush drawing without reprocessing input.
|
|
self.cv.update_idletasks()
|
|
return
|
|
self._updating = True
|
|
try:
|
|
self.cv.update()
|
|
finally:
|
|
self._updating = False
|
|
|
|
def _delay(self, delay):
|
|
"""Delay subsequent canvas actions for delay ms."""
|
|
self.cv.after(delay)
|
|
|
|
def _iscolorstring(self, color):
|
|
"""Check if the string color is a legal Tkinter color string.
|
|
"""
|
|
try:
|
|
self.cv.winfo_rgb(color)
|
|
ok = True
|
|
except TK.TclError:
|
|
ok = False
|
|
return ok
|
|
|
|
def _bgcolor(self, color=None):
|
|
"""Set canvas' backgroundcolor if color is not None,
|
|
else return backgroundcolor."""
|
|
if color is not None:
|
|
self.cv.config(bg = color)
|
|
self._update()
|
|
else:
|
|
return self.cv.cget("bg")
|
|
|
|
def _write(self, pos, txt, align, font, pencolor):
|
|
"""Write txt at pos on the canvas with specified font and color.
|
|
|
|
Return text item and x-coord of right bottom corner of text's
|
|
bounding box.
|
|
"""
|
|
x, y = pos
|
|
x = x * self.xscale
|
|
y = y * self.yscale
|
|
anchor = {"left":"sw", "center":"s", "right":"se" }
|
|
item = self.cv.create_text(x-1, -y, text = txt, anchor = anchor[align],
|
|
fill = pencolor, font = font)
|
|
x0, y0, x1, y1 = self.cv.bbox(item)
|
|
return item, x1-1
|
|
|
|
def _onclick(self, item, fun, num=1, add=None):
|
|
"""Bind fun to mouse-click event on turtle.
|
|
fun must be a function with two arguments, the coordinates
|
|
of the clicked point on the canvas.
|
|
num, the number of the mouse-button defaults to 1
|
|
"""
|
|
if fun is None:
|
|
self.cv.tag_unbind(item, "<Button-%s>" % num)
|
|
else:
|
|
def eventfun(event):
|
|
x, y = (self.cv.canvasx(event.x)/self.xscale,
|
|
-self.cv.canvasy(event.y)/self.yscale)
|
|
fun(x, y)
|
|
self.cv.tag_bind(item, "<Button-%s>" % num, eventfun, add)
|
|
|
|
def _onrelease(self, item, fun, num=1, add=None):
|
|
"""Bind fun to mouse-button-release event on turtle.
|
|
fun must be a function with two arguments, the coordinates
|
|
of the point on the canvas where mouse button is released.
|
|
num, the number of the mouse-button defaults to 1
|
|
|
|
If a turtle is clicked, first _onclick-event will be performed,
|
|
then _onscreensclick-event.
|
|
"""
|
|
if fun is None:
|
|
self.cv.tag_unbind(item, "<Button%s-ButtonRelease>" % num)
|
|
else:
|
|
def eventfun(event):
|
|
x, y = (self.cv.canvasx(event.x)/self.xscale,
|
|
-self.cv.canvasy(event.y)/self.yscale)
|
|
fun(x, y)
|
|
self.cv.tag_bind(item, "<Button%s-ButtonRelease>" % num,
|
|
eventfun, add)
|
|
|
|
def _ondrag(self, item, fun, num=1, add=None):
|
|
"""Bind fun to mouse-move-event (with pressed mouse button) on turtle.
|
|
fun must be a function with two arguments, the coordinates of the
|
|
actual mouse position on the canvas.
|
|
num, the number of the mouse-button defaults to 1
|
|
|
|
Every sequence of mouse-move-events on a turtle is preceded by a
|
|
mouse-click event on that turtle.
|
|
"""
|
|
if fun is None:
|
|
self.cv.tag_unbind(item, "<Button%s-Motion>" % num)
|
|
else:
|
|
def eventfun(event):
|
|
try:
|
|
x, y = (self.cv.canvasx(event.x)/self.xscale,
|
|
-self.cv.canvasy(event.y)/self.yscale)
|
|
fun(x, y)
|
|
except Exception:
|
|
pass
|
|
self.cv.tag_bind(item, "<Button%s-Motion>" % num, eventfun, add)
|
|
|
|
def _onscreenclick(self, fun, num=1, add=None):
|
|
"""Bind fun to mouse-click event on canvas.
|
|
fun must be a function with two arguments, the coordinates
|
|
of the clicked point on the canvas.
|
|
num, the number of the mouse-button defaults to 1
|
|
|
|
If a turtle is clicked, first _onclick-event will be performed,
|
|
then _onscreensclick-event.
|
|
"""
|
|
if fun is None:
|
|
self.cv.unbind("<Button-%s>" % num)
|
|
else:
|
|
def eventfun(event):
|
|
x, y = (self.cv.canvasx(event.x)/self.xscale,
|
|
-self.cv.canvasy(event.y)/self.yscale)
|
|
fun(x, y)
|
|
self.cv.bind("<Button-%s>" % num, eventfun, add)
|
|
|
|
def _onkeyrelease(self, fun, key):
|
|
"""Bind fun to key-release event of key.
|
|
Canvas must have focus. See method listen
|
|
"""
|
|
if fun is None:
|
|
self.cv.unbind("<KeyRelease-%s>" % key, None)
|
|
else:
|
|
def eventfun(event):
|
|
fun()
|
|
self.cv.bind("<KeyRelease-%s>" % key, eventfun)
|
|
|
|
def _onkeypress(self, fun, key=None):
|
|
"""If key is given, bind fun to key-press event of key.
|
|
Otherwise bind fun to any key-press.
|
|
Canvas must have focus. See method listen.
|
|
"""
|
|
if fun is None:
|
|
if key is None:
|
|
self.cv.unbind("<KeyPress>", None)
|
|
else:
|
|
self.cv.unbind("<KeyPress-%s>" % key, None)
|
|
else:
|
|
def eventfun(event):
|
|
fun()
|
|
if key is None:
|
|
self.cv.bind("<KeyPress>", eventfun)
|
|
else:
|
|
self.cv.bind("<KeyPress-%s>" % key, eventfun)
|
|
|
|
def _listen(self):
|
|
"""Set focus on canvas (in order to collect key-events)
|
|
"""
|
|
self.cv.focus_force()
|
|
|
|
def _ontimer(self, fun, t):
|
|
"""Install a timer, which calls fun after t milliseconds.
|
|
"""
|
|
if t == 0:
|
|
self.cv.after_idle(fun)
|
|
else:
|
|
self.cv.after(t, fun)
|
|
|
|
def _createimage(self, image):
|
|
"""Create and return image item on canvas.
|
|
"""
|
|
return self.cv.create_image(0, 0, image=image)
|
|
|
|
def _drawimage(self, item, pos, image):
|
|
"""Configure image item as to draw image object
|
|
at position (x,y) on canvas)
|
|
"""
|
|
x, y = pos
|
|
self.cv.coords(item, (x * self.xscale, -y * self.yscale))
|
|
self.cv.itemconfig(item, image=image)
|
|
|
|
def _setbgpic(self, item, image):
|
|
"""Configure image item as to draw image object
|
|
at center of canvas. Set item to the first item
|
|
in the displaylist, so it will be drawn below
|
|
any other item ."""
|
|
self.cv.itemconfig(item, image=image)
|
|
self.cv.tag_lower(item)
|
|
|
|
def _type(self, item):
|
|
"""Return 'line' or 'polygon' or 'image' depending on
|
|
type of item.
|
|
"""
|
|
return self.cv.type(item)
|
|
|
|
def _pointlist(self, item):
|
|
"""Return list of coordinate pairs of points of item.
|
|
|
|
For example:
|
|
>>> getscreen()._pointlist(getturtle().turtle._item)
|
|
[(0.0, 9.9999999999999982), (0.0, -9.9999999999999982),
|
|
(9.9999999999999982, 0.0)]
|
|
"""
|
|
cl = self.cv.coords(item)
|
|
pl = [(cl[i], -cl[i+1]) for i in range(0, len(cl), 2)]
|
|
return pl
|
|
|
|
def _setscrollregion(self, srx1, sry1, srx2, sry2):
|
|
self.cv.config(scrollregion=(srx1, sry1, srx2, sry2))
|
|
|
|
def _rescale(self, xscalefactor, yscalefactor):
|
|
items = self.cv.find_all()
|
|
for item in items:
|
|
coordinates = list(self.cv.coords(item))
|
|
newcoordlist = []
|
|
while coordinates:
|
|
x, y = coordinates[:2]
|
|
newcoordlist.append(x * xscalefactor)
|
|
newcoordlist.append(y * yscalefactor)
|
|
coordinates = coordinates[2:]
|
|
self.cv.coords(item, *newcoordlist)
|
|
|
|
def _resize(self, canvwidth=None, canvheight=None, bg=None):
|
|
"""Resize the canvas the turtles are drawing on.
|
|
|
|
Does not alter the drawing window.
|
|
"""
|
|
# needs amendment
|
|
if not isinstance(self.cv, ScrolledCanvas):
|
|
return self.canvwidth, self.canvheight
|
|
if canvwidth is canvheight is bg is None:
|
|
return self.cv.canvwidth, self.cv.canvheight
|
|
if canvwidth is not None:
|
|
self.canvwidth = canvwidth
|
|
if canvheight is not None:
|
|
self.canvheight = canvheight
|
|
self.cv.reset(canvwidth, canvheight, bg)
|
|
|
|
def _window_size(self):
|
|
"""Return the width and height of the turtle window."""
|
|
width = self.cv.winfo_width()
|
|
if width <= 1: # the window isn't managed by a geometry manager
|
|
width = self.cv['width']
|
|
height = self.cv.winfo_height()
|
|
if height <= 1: # the window isn't managed by a geometry manager
|
|
height = self.cv['height']
|
|
return width, height
|
|
|
|
def mainloop(self):
|
|
"""Start the event loop by calling Tkinter's mainloop function.
|
|
|
|
Must be the last statement in a turtle graphics program. Must not
|
|
be used if a script is run from within IDLE in -n mode (no
|
|
subprocess).
|
|
"""
|
|
self.cv.tk.mainloop()
|
|
|
|
def textinput(self, title, prompt):
|
|
"""Pop up a dialog window for input of a string.
|
|
|
|
title is the title of the dialog window and prompt is a text
|
|
describing what information to input. Return the string entered.
|
|
If the dialog is canceled, return None.
|
|
|
|
Arguments:
|
|
title -- a string
|
|
prompt -- a string
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
>>> screen.textinput("NIM", "Name of first player:")
|
|
"""
|
|
return simpledialog.askstring(title, prompt, parent=self.cv)
|
|
|
|
def numinput(self, title, prompt, default=None, minval=None, maxval=None):
|
|
"""Pop up a dialog window for input of a number.
|
|
|
|
title is the title of the dialog window and prompt is a text
|
|
describing what numerical information to input. default is the
|
|
default value, minval and maxval are the minimum and maximum
|
|
values for input. If these are given, the number entered must be
|
|
between minval and maxval. If not, a hint is issued and the dialog
|
|
remains open for correction. Return the number entered.
|
|
If the dialog is canceled, return None.
|
|
|
|
Arguments:
|
|
title -- a string
|
|
prompt -- a string
|
|
default -- a number or None
|
|
minval -- a number or None
|
|
maxval -- a number or None
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
>>> screen.numinput("Poker", "Your stakes:", 1000,
|
|
... minval=10, maxval=10000)
|
|
"""
|
|
return simpledialog.askfloat(title, prompt, initialvalue=default,
|
|
minvalue=minval, maxvalue=maxval,
|
|
parent=self.cv)
|
|
|
|
|
|
##############################################################################
|
|
### End of Tkinter - interface ###
|
|
##############################################################################
|
|
|
|
|
|
class Terminator(Exception):
|
|
"""Will be raised in TurtleScreen.update, if _RUNNING becomes False.
|
|
|
|
This stops execution of a turtle graphics script.
|
|
Main purpose: use in the Demo-Viewer turtle.Demo.py.
|
|
"""
|
|
pass
|
|
|
|
|
|
class TurtleGraphicsError(Exception):
|
|
"""Raised for invalid arguments or operations."""
|
|
|
|
|
|
class Shape:
|
|
"""Data structure modeling shapes.
|
|
|
|
type_ is one of the strings "polygon", "image" or "compound". What
|
|
data must be depends on type_: for "polygon" a tuple of pairs of
|
|
coordinates, for "image" an image (only used internally), and for
|
|
"compound" None, as a compound shape is built up using the
|
|
addcomponent method.
|
|
"""
|
|
def __init__(self, type_, data=None):
|
|
self._type = type_
|
|
if type_ == "polygon":
|
|
if isinstance(data, list):
|
|
data = tuple(data)
|
|
elif type_ == "image":
|
|
assert(isinstance(data, TK.PhotoImage))
|
|
elif type_ == "compound":
|
|
data = []
|
|
else:
|
|
raise TurtleGraphicsError("There is no shape type %s" % type_)
|
|
self._data = data
|
|
|
|
def addcomponent(self, poly, fill, outline=None):
|
|
"""Add a component to a shape of type "compound".
|
|
|
|
poly is a polygon, a tuple of pairs of numbers. fill is the color
|
|
the polygon is filled with and outline is the color of its
|
|
outline. If outline is not given, fill is used for it as well.
|
|
|
|
Arguments:
|
|
poly -- a tuple of pairs of numbers
|
|
fill -- a color
|
|
outline -- a color or None
|
|
|
|
Example:
|
|
>>> poly = ((0,0), (10,-5), (0,10), (-10,-5))
|
|
>>> s = Shape("compound")
|
|
>>> s.addcomponent(poly, "red", "blue")
|
|
>>> # ... add more components and then use register_shape()
|
|
"""
|
|
if self._type != "compound":
|
|
raise TurtleGraphicsError("Cannot add component to %s Shape"
|
|
% self._type)
|
|
if outline is None:
|
|
outline = fill
|
|
self._data.append([poly, fill, outline])
|
|
|
|
|
|
class Tbuffer:
|
|
"""Ring buffer used as undo buffer for RawTurtle objects."""
|
|
|
|
def __init__(self, bufsize=10):
|
|
self.bufsize = bufsize
|
|
self.buffer = [[None]] * bufsize
|
|
self.ptr = -1
|
|
self.cumulate = False
|
|
|
|
def reset(self, bufsize=None):
|
|
if bufsize is None:
|
|
for i in range(self.bufsize):
|
|
self.buffer[i] = [None]
|
|
else:
|
|
self.bufsize = bufsize
|
|
self.buffer = [[None]] * bufsize
|
|
self.ptr = -1
|
|
|
|
def push(self, item):
|
|
if self.bufsize > 0:
|
|
if not self.cumulate:
|
|
self.ptr = (self.ptr + 1) % self.bufsize
|
|
self.buffer[self.ptr] = item
|
|
else:
|
|
self.buffer[self.ptr].append(item)
|
|
|
|
def pop(self):
|
|
if self.bufsize > 0:
|
|
item = self.buffer[self.ptr]
|
|
if item is None:
|
|
return None
|
|
else:
|
|
self.buffer[self.ptr] = [None]
|
|
self.ptr = (self.ptr - 1) % self.bufsize
|
|
return (item)
|
|
|
|
def remove(self, item):
|
|
if item not in self.buffer:
|
|
return
|
|
index = self.buffer.index(item)
|
|
self.buffer.remove(item)
|
|
if index <= self.ptr:
|
|
self.ptr = (self.ptr - 1) % self.bufsize
|
|
self.buffer.insert((self.ptr+1) % self.bufsize, [None])
|
|
|
|
def nr_of_items(self):
|
|
return self.bufsize - self.buffer.count([None])
|
|
|
|
def __repr__(self):
|
|
return str(self.buffer) + " " + str(self.ptr)
|
|
|
|
|
|
|
|
class TurtleScreen(TurtleScreenBase):
|
|
"""Provides screen oriented methods like bgcolor etc.
|
|
|
|
Only relies upon the methods of TurtleScreenBase and not upon
|
|
components of the underlying graphics toolkit, which is Tkinter
|
|
in this case.
|
|
"""
|
|
_RUNNING = True
|
|
|
|
def __init__(self, cv, mode=_CFG["mode"],
|
|
colormode=_CFG["colormode"], delay=_CFG["delay"]):
|
|
TurtleScreenBase.__init__(self, cv)
|
|
|
|
self._shapes = {
|
|
"arrow" : Shape("polygon", ((-10,0), (10,0), (0,10))),
|
|
"turtle" : Shape("polygon", ((0,16), (-2,14), (-1,10), (-4,7),
|
|
(-7,9), (-9,8), (-6,5), (-7,1), (-5,-3), (-8,-6),
|
|
(-6,-8), (-4,-5), (0,-7), (4,-5), (6,-8), (8,-6),
|
|
(5,-3), (7,1), (6,5), (9,8), (7,9), (4,7), (1,10),
|
|
(2,14))),
|
|
"circle" : Shape("polygon", ((10,0), (9.51,3.09), (8.09,5.88),
|
|
(5.88,8.09), (3.09,9.51), (0,10), (-3.09,9.51),
|
|
(-5.88,8.09), (-8.09,5.88), (-9.51,3.09), (-10,0),
|
|
(-9.51,-3.09), (-8.09,-5.88), (-5.88,-8.09),
|
|
(-3.09,-9.51), (-0.00,-10.00), (3.09,-9.51),
|
|
(5.88,-8.09), (8.09,-5.88), (9.51,-3.09))),
|
|
"square" : Shape("polygon", ((10,-10), (10,10), (-10,10),
|
|
(-10,-10))),
|
|
"triangle" : Shape("polygon", ((10,-5.77), (0,11.55),
|
|
(-10,-5.77))),
|
|
"classic": Shape("polygon", ((0,0),(-5,-9),(0,-7),(5,-9))),
|
|
"blank" : Shape("image", self._blankimage())
|
|
}
|
|
|
|
self._bgpics = {"nopic" : ""}
|
|
|
|
self._mode = mode
|
|
self._delayvalue = delay
|
|
self._colormode = _CFG["colormode"]
|
|
self._keys = []
|
|
self.clear()
|
|
if sys.platform == 'darwin':
|
|
# Force Turtle window to the front on OS X. This is needed because
|
|
# the Turtle window will show behind the Terminal window when you
|
|
# start the demo from the command line.
|
|
rootwindow = cv.winfo_toplevel()
|
|
rootwindow.wm_attributes(topmost=True)
|
|
rootwindow.wm_attributes(topmost=False)
|
|
|
|
# A new screen means that turtle graphics is running again.
|
|
TurtleScreen._RUNNING = True
|
|
|
|
def clear(self):
|
|
"""Delete all drawings and all turtles from the TurtleScreen.
|
|
|
|
Aliases: clear | clearscreen
|
|
|
|
Reset the now empty TurtleScreen to its initial state: white
|
|
background, no background image, no event bindings and tracing on.
|
|
As a function, this method is only available under the name
|
|
clearscreen. The function clear is derived from the Turtle method
|
|
clear.
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
>>> screen.clearscreen()
|
|
"""
|
|
self._delayvalue = _CFG["delay"]
|
|
self._colormode = _CFG["colormode"]
|
|
self._delete("all")
|
|
self._bgpic = self._createimage("")
|
|
self._bgpicname = "nopic"
|
|
self._tracing = 1
|
|
self._updatecounter = 0
|
|
self._turtles = []
|
|
self.bgcolor("white")
|
|
for btn in 1, 2, 3:
|
|
self.onclick(None, btn)
|
|
self.onkeypress(None)
|
|
for key in self._keys[:]:
|
|
self.onkey(None, key)
|
|
self.onkeypress(None, key)
|
|
Turtle._pen = None
|
|
|
|
def mode(self, mode=None):
|
|
"""Set the turtle mode and perform a reset.
|
|
|
|
mode is one of the strings "standard", "logo" or "world". If mode
|
|
is not given, return the current mode.
|
|
|
|
Mode "standard" is compatible with the old turtle module. Mode
|
|
"logo" is compatible with most Logo turtle graphics. Mode "world"
|
|
uses user-defined world coordinates, see setworldcoordinates(). In
|
|
this mode, angles appear distorted if the x and y units are not of
|
|
equal length.
|
|
|
|
Mode Initial turtle heading positive angles
|
|
------------|-------------------------|-------------------
|
|
"standard" to the right (east) counterclockwise
|
|
"logo" upward (north) clockwise
|
|
|
|
Argument:
|
|
mode -- a string or None
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
>>> screen.mode("logo") # resets turtle heading to north
|
|
>>> screen.mode()
|
|
'logo'
|
|
"""
|
|
if mode is None:
|
|
return self._mode
|
|
mode = mode.lower()
|
|
if mode not in ["standard", "logo", "world"]:
|
|
raise TurtleGraphicsError("No turtle-graphics-mode %s" % mode)
|
|
self._mode = mode
|
|
if mode in ["standard", "logo"]:
|
|
self._setscrollregion(-self.canvwidth//2, -self.canvheight//2,
|
|
self.canvwidth//2, self.canvheight//2)
|
|
self.xscale = self.yscale = 1.0
|
|
self.reset()
|
|
|
|
def setworldcoordinates(self, llx, lly, urx, ury):
|
|
"""Set up a user-defined coordinate system.
|
|
|
|
llx and lly are the x and y coordinates of the lower left corner of
|
|
the canvas. urx and ury are the x and y coordinates of the upper
|
|
right corner. Switch to mode "world" if necessary, which performs a
|
|
reset. If mode "world" is already active, all drawings are redrawn
|
|
according to the new coordinates. In user-defined coordinate
|
|
systems, angles may appear distorted, see mode().
|
|
|
|
Arguments:
|
|
llx -- a number
|
|
lly -- a number
|
|
urx -- a number
|
|
ury -- a number
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
>>> screen.reset()
|
|
>>> screen.setworldcoordinates(-50,-7.5,50,7.5)
|
|
>>> for _ in range(72):
|
|
... left(10)
|
|
...
|
|
>>> for _ in range(8):
|
|
... left(45); fd(2) # a regular octagon
|
|
"""
|
|
if self.mode() != "world":
|
|
self.mode("world")
|
|
xspan = float(urx - llx)
|
|
yspan = float(ury - lly)
|
|
wx, wy = self._window_size()
|
|
self.screensize(wx-20, wy-20)
|
|
oldxscale, oldyscale = self.xscale, self.yscale
|
|
self.xscale = self.canvwidth / xspan
|
|
self.yscale = self.canvheight / yspan
|
|
srx1 = llx * self.xscale
|
|
sry1 = -ury * self.yscale
|
|
srx2 = self.canvwidth + srx1
|
|
sry2 = self.canvheight + sry1
|
|
self._setscrollregion(srx1, sry1, srx2, sry2)
|
|
self._rescale(self.xscale/oldxscale, self.yscale/oldyscale)
|
|
self.update()
|
|
|
|
def register_shape(self, name, shape=None):
|
|
"""Add a turtle shape to the TurtleScreen's shape list.
|
|
|
|
Aliases: register_shape | addshape
|
|
|
|
It can be called in four different ways:
|
|
|
|
(1) name is the name of an image file (PNG, GIF, PGM, and PPM) and
|
|
shape is None. Install the corresponding image shape.
|
|
(2) name is an arbitrary string and shape is the name of an image
|
|
file (PNG, GIF, PGM, and PPM). Install the corresponding image
|
|
shape.
|
|
(3) name is an arbitrary string and shape is a tuple of pairs of
|
|
coordinates. Install the corresponding polygon shape.
|
|
(4) name is an arbitrary string and shape is a (compound) Shape
|
|
object. Install the corresponding compound shape.
|
|
|
|
Image shapes do not rotate when turning the turtle, so they do not
|
|
display the heading of the turtle. To use a registered shape, call
|
|
shape() with its name.
|
|
|
|
Arguments:
|
|
name -- a string
|
|
shape -- a string, a tuple of pairs of numbers, a Shape object
|
|
or None
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
>>> screen.register_shape("turtle.gif")
|
|
>>> screen.register_shape("turtle", "turtle.gif")
|
|
>>> screen.register_shape("triangle", ((5,-3), (0,5), (-5,-3)))
|
|
"""
|
|
if shape is None:
|
|
shape = Shape("image", self._image(name))
|
|
elif isinstance(shape, str):
|
|
shape = Shape("image", self._image(shape))
|
|
elif isinstance(shape, tuple):
|
|
shape = Shape("polygon", shape)
|
|
## else shape assumed to be Shape-instance
|
|
self._shapes[name] = shape
|
|
|
|
def _colorstr(self, color):
|
|
"""Return the color string corresponding to color.
|
|
|
|
color may be a string or a tuple of three numbers, each between 0 and
|
|
the current colormode.
|
|
|
|
If color doesn't represent a color, an error is raised.
|
|
"""
|
|
if len(color) == 1:
|
|
color = color[0]
|
|
if isinstance(color, str):
|
|
if self._iscolorstring(color) or color == "":
|
|
return color
|
|
else:
|
|
raise TurtleGraphicsError("bad color string: %s" % str(color))
|
|
try:
|
|
r, g, b = color
|
|
except (TypeError, ValueError):
|
|
raise TurtleGraphicsError("bad color arguments: %s" % str(color))
|
|
if self._colormode == 1.0:
|
|
r, g, b = [round(255.0*x) for x in (r, g, b)]
|
|
if not ((0 <= r <= 255) and (0 <= g <= 255) and (0 <= b <= 255)):
|
|
raise TurtleGraphicsError("bad color sequence: %s" % str(color))
|
|
return "#%02x%02x%02x" % (r, g, b)
|
|
|
|
def _color(self, cstr):
|
|
if not cstr.startswith("#"):
|
|
return cstr
|
|
if len(cstr) == 7:
|
|
cl = [int(cstr[i:i+2], 16) for i in (1, 3, 5)]
|
|
elif len(cstr) == 4:
|
|
cl = [16*int(cstr[h], 16) for h in cstr[1:]]
|
|
else:
|
|
raise TurtleGraphicsError("bad colorstring: %s" % cstr)
|
|
return tuple(c * self._colormode/255 for c in cl)
|
|
|
|
def colormode(self, cmode=None):
|
|
"""Return the colormode or set it to 1.0 or 255.
|
|
|
|
After setting it, the r, g and b values of color triples have to
|
|
be between 0 and cmode.
|
|
|
|
Argument:
|
|
cmode -- a number or None
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
>>> screen.colormode(1)
|
|
>>> turtle.pencolor(240, 160, 80)
|
|
Traceback (most recent call last):
|
|
...
|
|
TurtleGraphicsError: bad color sequence: (240, 160, 80)
|
|
>>> screen.colormode()
|
|
1.0
|
|
>>> screen.colormode(255)
|
|
>>> screen.colormode()
|
|
255
|
|
>>> turtle.pencolor(240,160,80)
|
|
"""
|
|
if cmode is None:
|
|
return self._colormode
|
|
if cmode == 1.0:
|
|
self._colormode = float(cmode)
|
|
elif cmode == 255:
|
|
self._colormode = int(cmode)
|
|
|
|
def reset(self):
|
|
"""Reset all turtles on the screen to their initial state.
|
|
|
|
Aliases: reset | resetscreen
|
|
|
|
As a function, this method is only available under the name
|
|
resetscreen. The function reset is derived from the Turtle method
|
|
reset().
|
|
"""
|
|
for turtle in self._turtles:
|
|
turtle._setmode(self._mode)
|
|
turtle.reset()
|
|
|
|
def turtles(self):
|
|
"""Return the list of turtles on the screen.
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
>>> for turtle in screen.turtles():
|
|
... turtle.color("red")
|
|
"""
|
|
return self._turtles
|
|
|
|
def bgcolor(self, *args):
|
|
"""Return or set the background color of the TurtleScreen.
|
|
|
|
Four input formats are allowed:
|
|
- bgcolor()
|
|
Return the current background color as a color specification
|
|
string or as a tuple (see example). May be used as input
|
|
to another color(), pencolor(), fillcolor() or bgcolor() call.
|
|
- bgcolor(colorstring)
|
|
Set the background color to colorstring, which is a Tk color
|
|
specification string, such as "red", "yellow", or "#33cc8c".
|
|
- bgcolor((r, g, b))
|
|
Set the background color to the RGB color represented by
|
|
the tuple of r, g, and b. Each of r, g, and b must be
|
|
between 0 and colormode, where colormode is either 1.0 or 255
|
|
(see colormode()).
|
|
- bgcolor(r, g, b)
|
|
Set the background color to the RGB color represented by
|
|
r, g, and b. Each of r, g, and b must be between 0 and
|
|
colormode.
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
>>> screen.bgcolor("orange")
|
|
>>> screen.bgcolor()
|
|
'orange'
|
|
>>> screen.colormode(255)
|
|
>>> screen.bgcolor("#800080")
|
|
>>> screen.bgcolor()
|
|
(128.0, 0.0, 128.0)
|
|
"""
|
|
if args:
|
|
color = self._colorstr(args)
|
|
else:
|
|
color = None
|
|
color = self._bgcolor(color)
|
|
if color is not None:
|
|
color = self._color(color)
|
|
return color
|
|
|
|
def tracer(self, n=None, delay=None):
|
|
"""Turn turtle animation on or off and set the drawing delay.
|
|
|
|
If n is given, only every n-th regular screen update is performed,
|
|
which can be used to speed up the drawing of complex graphics. If
|
|
n is 0, animation is turned off and the screen is only updated by
|
|
calling update(). If delay is given, it sets the drawing delay, see
|
|
delay(). When called without arguments, return the current value
|
|
of n.
|
|
|
|
Arguments:
|
|
n -- a nonnegative integer or None
|
|
delay -- a nonnegative integer or None
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
>>> screen.tracer(8, 25)
|
|
>>> dist = 2
|
|
>>> for i in range(200):
|
|
... fd(dist)
|
|
... rt(90)
|
|
... dist += 2
|
|
"""
|
|
if n is None:
|
|
return self._tracing
|
|
self._tracing = int(n)
|
|
self._updatecounter = 0
|
|
if delay is not None:
|
|
self._delayvalue = int(delay)
|
|
if self._tracing:
|
|
self.update()
|
|
|
|
def delay(self, delay=None):
|
|
"""Return or set the drawing delay in milliseconds.
|
|
|
|
This is approximately the time interval between two consecutive
|
|
canvas updates. The longer the drawing delay, the slower the
|
|
animation.
|
|
|
|
Argument:
|
|
delay -- a nonnegative integer or None
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
>>> screen.delay()
|
|
10
|
|
>>> screen.delay(5)
|
|
>>> screen.delay()
|
|
5
|
|
"""
|
|
if delay is None:
|
|
return self._delayvalue
|
|
self._delayvalue = int(delay)
|
|
|
|
@contextmanager
|
|
def no_animation(self):
|
|
"""Temporarily turn off turtle animation.
|
|
|
|
The code inside the with block is not animated. Once the block is
|
|
exited, the drawing appears. This is useful for drawing complex
|
|
shapes where even the fastest speed setting is too slow.
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
>>> with screen.no_animation():
|
|
... for dist in range(2, 400, 2):
|
|
... fd(dist)
|
|
... rt(90)
|
|
"""
|
|
tracer = self.tracer()
|
|
try:
|
|
self.tracer(0)
|
|
yield
|
|
finally:
|
|
self.tracer(tracer)
|
|
|
|
def _incrementudc(self):
|
|
"""Increment the update counter."""
|
|
if not TurtleScreen._RUNNING:
|
|
TurtleScreen._RUNNING = True
|
|
raise Terminator
|
|
if self._tracing > 0:
|
|
self._updatecounter += 1
|
|
self._updatecounter %= self._tracing
|
|
|
|
def update(self):
|
|
"""Perform a TurtleScreen update."""
|
|
tracing = self._tracing
|
|
self._tracing = True
|
|
for t in self.turtles():
|
|
t._update_data()
|
|
t._drawturtle()
|
|
self._tracing = tracing
|
|
self._update()
|
|
|
|
def window_width(self):
|
|
"""Return the width of the turtle window.
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
>>> screen.window_width()
|
|
640
|
|
"""
|
|
return self._window_size()[0]
|
|
|
|
def window_height(self):
|
|
"""Return the height of the turtle window.
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
>>> screen.window_height()
|
|
480
|
|
"""
|
|
return self._window_size()[1]
|
|
|
|
def getcanvas(self):
|
|
"""Return the Canvas of this TurtleScreen.
|
|
|
|
Example (for a Screen instance named screen):
|
|
>>> cv = screen.getcanvas()
|
|
>>> cv
|
|
<turtle.ScrolledCanvas object ...>
|
|
"""
|
|
return self.cv
|
|
|
|
def getshapes(self):
|
|
"""Return a list of names of all currently available turtle shapes.
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
>>> screen.getshapes()
|
|
['arrow', 'blank', 'circle', ..., 'turtle']
|
|
"""
|
|
return sorted(self._shapes.keys())
|
|
|
|
def onclick(self, fun, btn=1, add=None):
|
|
"""Bind fun to mouse-click events on this screen.
|
|
|
|
Aliases: onclick | onscreenclick
|
|
|
|
fun is called with the coordinates of the clicked point on the
|
|
canvas. If fun is None, existing bindings are removed. btn is the
|
|
number of the mouse button and defaults to 1, the left mouse
|
|
button. If add is True, a new binding is added, otherwise it
|
|
replaces a former binding. As a function, this method is only
|
|
available under the name onscreenclick. The function onclick is
|
|
derived from the Turtle method onclick.
|
|
|
|
Arguments:
|
|
fun -- a function with two arguments or None
|
|
btn -- an integer
|
|
add -- a boolean or None
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
>>> screen.onscreenclick(turtle.goto)
|
|
>>> # Subsequently clicking into the TurtleScreen will
|
|
>>> # make the turtle move to the clicked point.
|
|
>>> screen.onscreenclick(None) # remove event binding again
|
|
"""
|
|
self._onscreenclick(fun, btn, add)
|
|
|
|
def onkey(self, fun, key):
|
|
"""Bind fun to key-release event of key.
|
|
|
|
Arguments:
|
|
fun -- a function with no arguments
|
|
key -- a string: key (e.g. "a") or key-symbol (e.g. "space")
|
|
|
|
In order to be able to register key-events, TurtleScreen
|
|
must have focus. (See method listen.)
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
|
|
>>> def f():
|
|
... fd(50)
|
|
... lt(60)
|
|
...
|
|
>>> screen.onkey(f, "Up")
|
|
>>> screen.listen()
|
|
|
|
Subsequently the turtle can be moved by repeatedly pressing
|
|
the up-arrow key, consequently drawing a hexagon
|
|
|
|
"""
|
|
if fun is None:
|
|
if key in self._keys:
|
|
self._keys.remove(key)
|
|
elif key not in self._keys:
|
|
self._keys.append(key)
|
|
self._onkeyrelease(fun, key)
|
|
|
|
def onkeypress(self, fun, key=None):
|
|
"""Bind fun to key-press event of key if key is given,
|
|
or to any key-press-event if no key is given.
|
|
|
|
Arguments:
|
|
fun -- a function with no arguments
|
|
key -- a string: key (e.g. "a") or key-symbol (e.g. "space")
|
|
|
|
In order to be able to register key-events, TurtleScreen
|
|
must have focus. (See method listen.)
|
|
|
|
Example (for a TurtleScreen instance named screen
|
|
and a Turtle instance named turtle):
|
|
|
|
>>> def f():
|
|
... fd(50)
|
|
... lt(60)
|
|
...
|
|
>>> screen.onkeypress(f, "Up")
|
|
>>> screen.listen()
|
|
|
|
Subsequently the turtle can be moved by repeatedly pressing
|
|
the up-arrow key, or by keeping pressed the up-arrow key.
|
|
consequently drawing a hexagon.
|
|
"""
|
|
if fun is None:
|
|
if key in self._keys:
|
|
self._keys.remove(key)
|
|
elif key is not None and key not in self._keys:
|
|
self._keys.append(key)
|
|
self._onkeypress(fun, key)
|
|
|
|
def listen(self, xdummy=None, ydummy=None):
|
|
"""Set focus on TurtleScreen (in order to collect key-events)
|
|
|
|
No arguments.
|
|
Dummy arguments are provided in order
|
|
to be able to pass listen to the onclick method.
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
>>> screen.listen()
|
|
"""
|
|
self._listen()
|
|
|
|
def ontimer(self, fun, t=0):
|
|
"""Install a timer, which calls fun after t milliseconds.
|
|
|
|
Arguments:
|
|
fun -- a function with no arguments.
|
|
t -- a number >= 0
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
|
|
>>> running = True
|
|
>>> def f():
|
|
... if running:
|
|
... fd(50)
|
|
... lt(60)
|
|
... screen.ontimer(f, 250)
|
|
...
|
|
>>> f() # makes the turtle marching around
|
|
>>> running = False
|
|
"""
|
|
self._ontimer(fun, t)
|
|
|
|
def bgpic(self, picname=None):
|
|
"""Set background image or return name of current backgroundimage.
|
|
|
|
Optional argument:
|
|
picname -- a string, name of an image file (PNG, GIF, PGM, and PPM) or "nopic".
|
|
|
|
If picname is a filename, set the corresponding image as background.
|
|
If picname is "nopic", delete backgroundimage, if present.
|
|
If picname is None, return the filename of the current backgroundimage.
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
>>> screen.bgpic()
|
|
'nopic'
|
|
>>> screen.bgpic("landscape.gif")
|
|
>>> screen.bgpic()
|
|
'landscape.gif'
|
|
"""
|
|
if picname is None:
|
|
return self._bgpicname
|
|
if picname not in self._bgpics:
|
|
self._bgpics[picname] = self._image(picname)
|
|
self._setbgpic(self._bgpic, self._bgpics[picname])
|
|
self._bgpicname = picname
|
|
|
|
def screensize(self, canvwidth=None, canvheight=None, bg=None):
|
|
"""Resize the canvas the turtles are drawing on.
|
|
|
|
Optional arguments:
|
|
canvwidth -- positive integer, new width of canvas in pixels
|
|
canvheight -- positive integer, new height of canvas in pixels
|
|
bg -- colorstring or color-tuple, new backgroundcolor
|
|
If no arguments are given, return current (canvaswidth, canvasheight)
|
|
|
|
Do not alter the drawing window. To observe hidden parts of
|
|
the canvas use the scrollbars. (Can make visible those parts
|
|
of a drawing, which were outside the canvas before!)
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.screensize(2000,1500)
|
|
>>> # e.g. to search for an erroneously escaped turtle ;-)
|
|
"""
|
|
return self._resize(canvwidth, canvheight, bg)
|
|
|
|
def save(self, filename, *, overwrite=False):
|
|
"""Save the drawing as a PostScript file
|
|
|
|
Arguments:
|
|
filename -- a string, the path of the created file.
|
|
Must end with '.ps' or '.eps'.
|
|
|
|
Optional arguments:
|
|
overwrite -- boolean, if true, then existing files will be overwritten
|
|
|
|
Example (for a TurtleScreen instance named screen):
|
|
>>> screen.save('my_drawing.eps')
|
|
"""
|
|
filename = Path(filename)
|
|
if not filename.parent.exists():
|
|
raise FileNotFoundError(
|
|
f"The directory '{filename.parent}' does not exist."
|
|
" Cannot save to it."
|
|
)
|
|
if not overwrite and filename.exists():
|
|
raise FileExistsError(
|
|
f"The file '{filename}' already exists. To overwrite it use"
|
|
" the 'overwrite=True' argument of the save function."
|
|
)
|
|
if (ext := filename.suffix) not in {".ps", ".eps"}:
|
|
raise ValueError(
|
|
f"Unknown file extension: '{ext}',"
|
|
" must be one of {'.ps', '.eps'}"
|
|
)
|
|
|
|
postscript = self.cv.postscript()
|
|
filename.write_text(postscript)
|
|
|
|
onscreenclick = onclick
|
|
resetscreen = reset
|
|
clearscreen = clear
|
|
addshape = register_shape
|
|
onkeyrelease = onkey
|
|
|
|
class TNavigator:
|
|
"""The navigation part of RawTurtle.
|
|
|
|
Implements methods for turtle movement.
|
|
"""
|
|
START_ORIENTATION = {
|
|
"standard": Vec2D(1.0, 0.0),
|
|
"world" : Vec2D(1.0, 0.0),
|
|
"logo" : Vec2D(0.0, 1.0),
|
|
}
|
|
DEFAULT_MODE = "standard"
|
|
DEFAULT_ANGLEOFFSET = 0
|
|
DEFAULT_ANGLEORIENT = 1
|
|
|
|
def __init__(self, mode=DEFAULT_MODE):
|
|
self._angleOffset = self.DEFAULT_ANGLEOFFSET
|
|
self._angleOrient = self.DEFAULT_ANGLEORIENT
|
|
self._mode = mode
|
|
self.undobuffer = None
|
|
self.degrees()
|
|
self._mode = None
|
|
self._setmode(mode)
|
|
TNavigator.reset(self)
|
|
|
|
def reset(self):
|
|
"""Reset the turtle to its initial values.
|
|
|
|
Will be overwritten by the child class.
|
|
"""
|
|
self._position = Vec2D(0.0, 0.0)
|
|
self._orient = TNavigator.START_ORIENTATION[self._mode]
|
|
|
|
def _setmode(self, mode=None):
|
|
"""Set the turtle's mode to 'standard', 'world' or 'logo'."""
|
|
if mode is None:
|
|
return self._mode
|
|
if mode not in ["standard", "logo", "world"]:
|
|
return
|
|
self._mode = mode
|
|
if mode in ["standard", "world"]:
|
|
self._angleOffset = 0
|
|
self._angleOrient = 1
|
|
else: # mode == "logo":
|
|
self._angleOffset = self._fullcircle/4.
|
|
self._angleOrient = -1
|
|
|
|
def _setDegreesPerAU(self, fullcircle):
|
|
"""Helper function for degrees() and radians()."""
|
|
self._fullcircle = fullcircle
|
|
self._degreesPerAU = 360/fullcircle
|
|
if self._mode == "standard":
|
|
self._angleOffset = 0
|
|
else:
|
|
self._angleOffset = fullcircle/4.
|
|
|
|
def degrees(self, fullcircle=360.0):
|
|
"""Set the angle measurement units to degrees.
|
|
|
|
The number of degrees in a full circle is set to fullcircle, which
|
|
defaults to 360.
|
|
|
|
Argument:
|
|
fullcircle -- a number
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.home()
|
|
>>> turtle.left(90)
|
|
>>> turtle.heading()
|
|
90.0
|
|
|
|
>>> # Change angle measurement unit to grad (also known as gon,
|
|
>>> # grade, or gradian and equals 1/100-th of the right angle.)
|
|
>>> turtle.degrees(400.0)
|
|
>>> turtle.heading()
|
|
100.0
|
|
>>> turtle.degrees(360)
|
|
>>> turtle.heading()
|
|
90.0
|
|
"""
|
|
self._setDegreesPerAU(fullcircle)
|
|
|
|
def radians(self):
|
|
"""Set the angle measurement units to radians.
|
|
|
|
Equivalent to degrees(2 * math.pi).
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.home()
|
|
>>> turtle.left(90)
|
|
>>> turtle.heading()
|
|
90.0
|
|
>>> turtle.radians()
|
|
>>> turtle.heading()
|
|
1.5707963267948966
|
|
"""
|
|
self._setDegreesPerAU(math.tau)
|
|
|
|
def _go(self, distance):
|
|
"""Move the turtle forward by the specified distance."""
|
|
ende = self._position + self._orient * distance
|
|
self._goto(ende)
|
|
|
|
def _rotate(self, angle):
|
|
"""Turn the turtle counterclockwise by specified angle, if the angle > 0."""
|
|
angle *= self._degreesPerAU
|
|
self._orient = self._orient.rotate(angle)
|
|
|
|
def _goto(self, end):
|
|
"""Move the turtle to the end position."""
|
|
self._position = end
|
|
|
|
@contextmanager
|
|
def _undo_sequence(self):
|
|
"""Record the enclosed actions as a single undo step."""
|
|
undobuffer = self.undobuffer
|
|
if not undobuffer or undobuffer.cumulate:
|
|
yield
|
|
return
|
|
undobuffer.push(["seq"])
|
|
undobuffer.cumulate = True
|
|
try:
|
|
yield
|
|
finally:
|
|
undobuffer.cumulate = False
|
|
|
|
def teleport(self, x=None, y=None, *, fill_gap: bool = False) -> None:
|
|
"""To be overwritten by the child class RawTurtle.
|
|
|
|
Includes no TPen references.
|
|
"""
|
|
new_x = x if x is not None else self._position[0]
|
|
new_y = y if y is not None else self._position[1]
|
|
self._position = Vec2D(new_x, new_y)
|
|
|
|
def forward(self, distance):
|
|
"""Move the turtle forward by the specified distance.
|
|
|
|
Aliases: forward | fd
|
|
|
|
The turtle moves in the direction it is headed.
|
|
|
|
Argument:
|
|
distance -- a number
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.position()
|
|
(0.00,0.00)
|
|
>>> turtle.forward(25)
|
|
>>> turtle.position()
|
|
(25.00,0.00)
|
|
>>> turtle.forward(-75)
|
|
>>> turtle.position()
|
|
(-50.00,0.00)
|
|
"""
|
|
self._go(distance)
|
|
|
|
def back(self, distance):
|
|
"""Move the turtle backward by distance.
|
|
|
|
Aliases: back | backward | bk
|
|
|
|
The turtle moves opposite to the direction it is headed.
|
|
The turtle's heading does not change.
|
|
|
|
Argument:
|
|
distance -- a number
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.position()
|
|
(0.00,0.00)
|
|
>>> turtle.backward(30)
|
|
>>> turtle.position()
|
|
(-30.00,0.00)
|
|
"""
|
|
self._go(-distance)
|
|
|
|
def right(self, angle):
|
|
"""Turn the turtle right by the specified angle.
|
|
|
|
Aliases: right | rt
|
|
|
|
The angle is measured in degrees by default; the unit can be
|
|
changed with degrees() or radians(). How the heading is measured
|
|
depends on the turtle mode, see mode().
|
|
|
|
Argument:
|
|
angle -- a number
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.heading()
|
|
22.0
|
|
>>> turtle.right(45)
|
|
>>> turtle.heading()
|
|
337.0
|
|
"""
|
|
self._rotate(-angle)
|
|
|
|
def left(self, angle):
|
|
"""Turn the turtle left by the specified angle.
|
|
|
|
Aliases: left | lt
|
|
|
|
The angle is measured in degrees by default; the unit can be
|
|
changed with degrees() or radians(). How the heading is measured
|
|
depends on the turtle mode, see mode().
|
|
|
|
Argument:
|
|
angle -- a number
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.heading()
|
|
22.0
|
|
>>> turtle.left(45)
|
|
>>> turtle.heading()
|
|
67.0
|
|
"""
|
|
self._rotate(angle)
|
|
|
|
def pos(self):
|
|
"""Return the turtle's current location (x,y) as a Vec2D vector.
|
|
|
|
Aliases: pos | position
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.pos()
|
|
(0.00,240.00)
|
|
"""
|
|
return self._position
|
|
|
|
def xcor(self):
|
|
"""Return the turtle's x coordinate.
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.home()
|
|
>>> turtle.left(50)
|
|
>>> turtle.forward(100)
|
|
>>> print(round(turtle.xcor(), 5))
|
|
64.27876
|
|
"""
|
|
return self._position[0]
|
|
|
|
def ycor(self):
|
|
"""Return the turtle's y coordinate
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> reset()
|
|
>>> turtle.left(60)
|
|
>>> turtle.forward(100)
|
|
>>> print(round(turtle.ycor(), 5))
|
|
86.60254
|
|
"""
|
|
return self._position[1]
|
|
|
|
|
|
def goto(self, x, y=None):
|
|
"""Move the turtle to an absolute position.
|
|
|
|
Aliases: goto | setpos | setposition
|
|
|
|
If y is None, x must be a pair of coordinates or a Vec2D, for
|
|
example as returned by pos(). If the pen is down, a line is drawn.
|
|
The turtle's heading does not change.
|
|
|
|
Arguments:
|
|
x -- a number or a pair/vector of numbers
|
|
y -- a number or None
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> tp = turtle.pos()
|
|
>>> tp
|
|
(0.00,0.00)
|
|
>>> turtle.goto(60,30)
|
|
>>> turtle.pos()
|
|
(60.00,30.00)
|
|
>>> turtle.goto((20,80))
|
|
>>> turtle.pos()
|
|
(20.00,80.00)
|
|
>>> turtle.goto(tp)
|
|
>>> turtle.pos()
|
|
(0.00,0.00)
|
|
"""
|
|
if y is None:
|
|
self._goto(Vec2D(*x))
|
|
else:
|
|
self._goto(Vec2D(x, y))
|
|
|
|
def home(self):
|
|
"""Move the turtle to the origin, coordinates (0,0).
|
|
|
|
The turtle's heading is set to its start orientation, which
|
|
depends on the turtle mode, see mode().
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.heading()
|
|
90.0
|
|
>>> turtle.position()
|
|
(0.00,-10.00)
|
|
>>> turtle.home()
|
|
>>> turtle.position()
|
|
(0.00,0.00)
|
|
>>> turtle.heading()
|
|
0.0
|
|
"""
|
|
self.goto(0, 0)
|
|
self.setheading(0)
|
|
|
|
def setx(self, x):
|
|
"""Set the turtle's x coordinate to x.
|
|
|
|
The y coordinate is unchanged.
|
|
|
|
Argument:
|
|
x -- a number
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.position()
|
|
(0.00,240.00)
|
|
>>> turtle.setx(10)
|
|
>>> turtle.position()
|
|
(10.00,240.00)
|
|
"""
|
|
self._goto(Vec2D(x, self._position[1]))
|
|
|
|
def sety(self, y):
|
|
"""Set the turtle's y coordinate to y.
|
|
|
|
The x coordinate is unchanged.
|
|
|
|
Argument:
|
|
y -- a number
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.position()
|
|
(0.00,40.00)
|
|
>>> turtle.sety(-10)
|
|
>>> turtle.position()
|
|
(0.00,-10.00)
|
|
"""
|
|
self._goto(Vec2D(self._position[0], y))
|
|
|
|
def distance(self, x, y=None):
|
|
"""Return the distance from the turtle to (x,y) in turtle step units.
|
|
|
|
If y is None, x must be a pair of coordinates, a Vec2D, for example
|
|
as returned by pos(), or another turtle.
|
|
|
|
Arguments:
|
|
x -- a number or a pair/vector of numbers or a turtle instance
|
|
y -- a number if x is a number, else None
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.home()
|
|
>>> turtle.distance(30,40)
|
|
50.0
|
|
>>> turtle.distance((30,40))
|
|
50.0
|
|
>>> joe = Turtle()
|
|
>>> joe.forward(77)
|
|
>>> turtle.distance(joe)
|
|
77.0
|
|
"""
|
|
if y is not None:
|
|
pos = Vec2D(x, y)
|
|
if isinstance(x, Vec2D):
|
|
pos = x
|
|
elif isinstance(x, tuple):
|
|
pos = Vec2D(*x)
|
|
elif isinstance(x, TNavigator):
|
|
pos = x._position
|
|
return abs(pos - self._position)
|
|
|
|
def towards(self, x, y=None):
|
|
"""Return the angle of the line from the turtle's position to (x,y).
|
|
|
|
If y is None, x must be a pair of coordinates, a Vec2D, for example
|
|
as returned by pos(), or another turtle. The angle is measured from
|
|
the turtle's start orientation, which depends on the turtle mode,
|
|
see mode().
|
|
|
|
Arguments:
|
|
x -- a number or a pair/vector of numbers or a turtle instance
|
|
y -- a number if x is a number, else None
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.goto(10, 10)
|
|
>>> turtle.towards(0,0)
|
|
225.0
|
|
"""
|
|
if y is not None:
|
|
pos = Vec2D(x, y)
|
|
if isinstance(x, Vec2D):
|
|
pos = x
|
|
elif isinstance(x, tuple):
|
|
pos = Vec2D(*x)
|
|
elif isinstance(x, TNavigator):
|
|
pos = x._position
|
|
x, y = pos - self._position
|
|
result = round(math.degrees(math.atan2(y, x)), 10) % 360.0
|
|
result /= self._degreesPerAU
|
|
return (self._angleOffset + self._angleOrient*result) % self._fullcircle
|
|
|
|
def heading(self):
|
|
"""Return the turtle's current heading.
|
|
|
|
The value depends on the turtle mode, see mode().
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.home()
|
|
>>> turtle.left(67)
|
|
>>> turtle.heading()
|
|
67.0
|
|
"""
|
|
x, y = self._orient
|
|
result = round(math.degrees(math.atan2(y, x)), 10) % 360.0
|
|
result /= self._degreesPerAU
|
|
return (self._angleOffset + self._angleOrient*result) % self._fullcircle
|
|
|
|
def setheading(self, to_angle):
|
|
"""Set the turtle's heading to to_angle.
|
|
|
|
Aliases: setheading | seth
|
|
|
|
Here are some common directions in degrees:
|
|
|
|
standard mode logo mode
|
|
-------------------|---------------
|
|
0 - east 0 - north
|
|
90 - north 90 - east
|
|
180 - west 180 - south
|
|
270 - south 270 - west
|
|
|
|
Argument:
|
|
to_angle -- a number
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.setheading(90)
|
|
>>> turtle.heading()
|
|
90.0
|
|
"""
|
|
angle = (to_angle - self.heading()) * self._angleOrient
|
|
full = self._fullcircle
|
|
angle = (angle+full/2.)%full - full/2.
|
|
self._rotate(angle)
|
|
|
|
def circle(self, radius, extent = None, steps = None):
|
|
"""Draw a circle with the given radius.
|
|
|
|
The center is radius units left of the turtle; extent, an angle,
|
|
determines which part of the circle is drawn. If extent is not
|
|
given, draw the entire circle. If extent is not a full circle, one
|
|
endpoint of the arc is the current pen position. Draw the arc in
|
|
the counterclockwise direction if radius is positive, otherwise in
|
|
the clockwise direction. Finally, the turtle's heading is changed by
|
|
extent.
|
|
|
|
As the circle is approximated by an inscribed regular polygon,
|
|
steps determines the number of steps to use. If not given, it
|
|
will be calculated automatically. May be used to draw regular
|
|
polygons.
|
|
|
|
Arguments:
|
|
radius -- a number
|
|
extent -- a number or None
|
|
steps -- an integer or None
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.circle(50)
|
|
>>> turtle.circle(120, 180) # draw a semicircle
|
|
"""
|
|
speed = self.speed()
|
|
if extent is None:
|
|
extent = self._fullcircle
|
|
if steps is None:
|
|
frac = abs(extent)/self._fullcircle
|
|
steps = 1 + int(min(11 + abs(radius) / 6.0, 59.0) * frac)
|
|
w = 1.0 * extent / steps
|
|
w2 = 0.5 * w
|
|
l = 2.0 * radius * math.sin(math.radians(w2)*self._degreesPerAU)
|
|
if radius < 0:
|
|
l, w, w2 = -l, -w, -w2
|
|
tr = self._tracer()
|
|
dl = self._delay()
|
|
with self._undo_sequence():
|
|
if speed == 0:
|
|
self._tracer(0, 0)
|
|
else:
|
|
self.speed(0)
|
|
self._rotate(w2)
|
|
for i in range(steps):
|
|
self.speed(speed)
|
|
self._go(l)
|
|
self.speed(0)
|
|
self._rotate(w)
|
|
self._rotate(-w2)
|
|
if speed == 0:
|
|
self._tracer(tr, dl)
|
|
self.speed(speed)
|
|
|
|
# Three dummy methods to be implemented by the child class:
|
|
|
|
def speed(self, s=0):
|
|
"""Dummy method - to be overwritten by the child class."""
|
|
def _tracer(self, a=None, b=None):
|
|
"""Dummy method - to be overwritten by the child class."""
|
|
def _delay(self, n=None):
|
|
"""Dummy method - to be overwritten by the child class."""
|
|
|
|
# Aliases for the commonly used commands:
|
|
|
|
fd = forward
|
|
bk = back
|
|
backward = back
|
|
rt = right
|
|
lt = left
|
|
position = pos
|
|
setpos = goto
|
|
setposition = goto
|
|
seth = setheading
|
|
|
|
|
|
class TPen:
|
|
"""Drawing part of the RawTurtle.
|
|
|
|
Implements drawing properties.
|
|
"""
|
|
|
|
def __init__(self, resizemode=_CFG["resizemode"]):
|
|
self._resizemode = resizemode # or "user" or "noresize"
|
|
self.undobuffer = None
|
|
TPen._reset(self)
|
|
|
|
def _reset(self, pencolor=_CFG["pencolor"],
|
|
fillcolor=_CFG["fillcolor"]):
|
|
self._pensize = 1
|
|
self._shown = True
|
|
self._pencolor = pencolor
|
|
self._fillcolor = fillcolor
|
|
self._drawing = True
|
|
self._speed = 3
|
|
self._stretchfactor = (1., 1.)
|
|
self._shearfactor = 0.
|
|
self._tilt = 0.
|
|
self._shapetrafo = (1., 0., 0., 1.)
|
|
self._outlinewidth = 1
|
|
|
|
def resizemode(self, rmode=None):
|
|
"""Set the resizemode to "auto", "user" or "noresize".
|
|
|
|
If rmode is not given, return the current resizemode. The
|
|
different resizemodes have the following effects:
|
|
|
|
- "auto": the turtle is resized according to the value of
|
|
pensize().
|
|
- "user": the turtle is resized according to the stretch factors
|
|
and outline width, which are set by shapesize().
|
|
- "noresize": the turtle is not resized.
|
|
|
|
resizemode("user") is called by shapesize() when used with
|
|
arguments.
|
|
|
|
Argument:
|
|
rmode -- a string or None
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.resizemode()
|
|
'noresize'
|
|
>>> turtle.resizemode("auto")
|
|
>>> turtle.resizemode()
|
|
'auto'
|
|
"""
|
|
if rmode is None:
|
|
return self._resizemode
|
|
rmode = rmode.lower()
|
|
if rmode in ["auto", "user", "noresize"]:
|
|
self.pen(resizemode=rmode)
|
|
|
|
def pensize(self, width=None):
|
|
"""Return or set the line thickness.
|
|
|
|
Aliases: pensize | width
|
|
|
|
If width is not given, return the current pensize. If resizemode
|
|
is set to "auto" and the turtle shape is a polygon, that polygon is
|
|
drawn with the same line thickness.
|
|
|
|
Argument:
|
|
width -- a positive number or None
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.pensize()
|
|
1
|
|
>>> turtle.pensize(10) # from here on lines of width 10 are drawn
|
|
"""
|
|
if width is None:
|
|
return self._pensize
|
|
self.pen(pensize=width)
|
|
|
|
|
|
def penup(self):
|
|
"""Lift the pen.
|
|
|
|
Aliases: penup | pu | up
|
|
|
|
The turtle does not draw when it moves.
|
|
"""
|
|
if not self._drawing:
|
|
return
|
|
self.pen(pendown=False)
|
|
|
|
def pendown(self):
|
|
"""Lower the pen.
|
|
|
|
Aliases: pendown | pd | down
|
|
|
|
The turtle draws a line when it moves.
|
|
"""
|
|
if self._drawing:
|
|
return
|
|
self.pen(pendown=True)
|
|
|
|
def isdown(self):
|
|
"""Return True if the pen is down, False if it's up.
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.penup()
|
|
>>> turtle.isdown()
|
|
False
|
|
>>> turtle.pendown()
|
|
>>> turtle.isdown()
|
|
True
|
|
"""
|
|
return self._drawing
|
|
|
|
def speed(self, speed=None):
|
|
"""Return or set the turtle's speed.
|
|
|
|
If speed is not given, return the current speed. Otherwise set the
|
|
turtle's speed to an integer from 0 to 10. If speed is a number
|
|
greater than 10 or smaller than 0.5, the speed is set to 0. You
|
|
can also use these names instead of numbers:
|
|
|
|
"fastest": 0
|
|
"fast": 10
|
|
"normal": 6
|
|
"slow": 3
|
|
"slowest": 1
|
|
|
|
Speeds from 1 to 10 give increasingly faster animation of line
|
|
drawing and turtle turning. A speed of 0 means that no animation
|
|
takes place: forward() and back() make the turtle jump, and
|
|
likewise left() and right() make the turtle turn instantly.
|
|
|
|
Argument:
|
|
speed -- an integer, a string or None
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.speed()
|
|
3
|
|
>>> turtle.speed('normal')
|
|
>>> turtle.speed()
|
|
6
|
|
>>> turtle.speed(9)
|
|
>>> turtle.speed()
|
|
9
|
|
"""
|
|
speed_strings = {
|
|
'fastest': 0,
|
|
'fast': 10,
|
|
'normal': 6,
|
|
'slow': 3,
|
|
'slowest': 1,
|
|
}
|
|
|
|
if speed is None:
|
|
return self._speed
|
|
if speed in speed_strings:
|
|
speed = speed_strings[speed]
|
|
elif 0.5 < speed < 10.5:
|
|
speed = int(round(speed))
|
|
else:
|
|
speed = 0
|
|
self.pen(speed=speed)
|
|
|
|
def color(self, *args):
|
|
"""Return or set the pencolor and fillcolor.
|
|
|
|
Several input formats are allowed. They use 0 to 3 arguments as follows:
|
|
- color()
|
|
Return the current pencolor and the current fillcolor as
|
|
a pair of color specification strings or tuples as returned
|
|
by pencolor() and fillcolor().
|
|
- color(colorstring), color((r,g,b)), color(r,g,b)
|
|
Inputs as in pencolor(). Set both the pencolor and the
|
|
fillcolor to the given value.
|
|
- color(colorstring1, colorstring2), color((r1,g1,b1), (r2,g2,b2))
|
|
Equivalent to pencolor(colorstring1) and fillcolor(colorstring2)
|
|
and analogously if the other input format is used.
|
|
|
|
If the turtle shape is a polygon, the outline and interior of that
|
|
polygon are drawn with the newly set colors. For more information,
|
|
see pencolor() and fillcolor().
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.color("red", "green")
|
|
>>> turtle.color()
|
|
('red', 'green')
|
|
>>> colormode(255)
|
|
>>> turtle.color("#285078", "#a0c8f0")
|
|
>>> turtle.color()
|
|
((40.0, 80.0, 120.0), (160.0, 200.0, 240.0))
|
|
"""
|
|
if args:
|
|
l = len(args)
|
|
if l == 1:
|
|
pcolor = fcolor = args[0]
|
|
elif l == 2:
|
|
pcolor, fcolor = args
|
|
elif l == 3:
|
|
pcolor = fcolor = args
|
|
pcolor = self._colorstr(pcolor)
|
|
fcolor = self._colorstr(fcolor)
|
|
self.pen(pencolor=pcolor, fillcolor=fcolor)
|
|
else:
|
|
return self._color(self._pencolor), self._color(self._fillcolor)
|
|
|
|
def pencolor(self, *args):
|
|
"""Return or set the pencolor.
|
|
|
|
Four input formats are allowed:
|
|
- pencolor()
|
|
Return the current pencolor as color specification string or
|
|
as a tuple (see example). May be used as input to another
|
|
color(), pencolor(), fillcolor() or bgcolor() call.
|
|
- pencolor(colorstring)
|
|
Set the pencolor to colorstring, which is a Tk color
|
|
specification string, such as "red", "yellow", or "#33cc8c".
|
|
- pencolor((r, g, b))
|
|
Set the pencolor to the RGB color represented by the tuple of
|
|
r, g, and b. Each of r, g, and b must be between 0 and
|
|
colormode, where colormode is either 1.0 or 255 (see
|
|
colormode()).
|
|
- pencolor(r, g, b)
|
|
Set the pencolor to the RGB color represented by r, g, and b.
|
|
Each of r, g, and b must be between 0 and colormode.
|
|
|
|
If the turtle shape is a polygon, the outline of that polygon is
|
|
drawn with the newly set pencolor.
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.pencolor("brown")
|
|
>>> turtle.pencolor()
|
|
'brown'
|
|
>>> colormode(255)
|
|
>>> turtle.pencolor("#32c18f")
|
|
>>> turtle.pencolor()
|
|
(50.0, 193.0, 143.0)
|
|
"""
|
|
if args:
|
|
color = self._colorstr(args)
|
|
if color == self._pencolor:
|
|
return
|
|
self.pen(pencolor=color)
|
|
else:
|
|
return self._color(self._pencolor)
|
|
|
|
def fillcolor(self, *args):
|
|
"""Return or set the fillcolor.
|
|
|
|
Four input formats are allowed:
|
|
- fillcolor()
|
|
Return the current fillcolor as color specification string,
|
|
possibly in tuple format (see example). May be used as
|
|
input to another color(), pencolor(), fillcolor() or bgcolor() call.
|
|
- fillcolor(colorstring)
|
|
Set the fillcolor to colorstring, which is a Tk color
|
|
specification string, such as "red", "yellow", or "#33cc8c".
|
|
- fillcolor((r, g, b))
|
|
Set the fillcolor to the RGB color represented by the tuple of
|
|
r, g, and b. Each of r, g, and b must be between 0 and
|
|
colormode, where colormode is either 1.0 or 255 (see
|
|
colormode()).
|
|
- fillcolor(r, g, b)
|
|
Set the fillcolor to the RGB color represented by r, g, and b.
|
|
Each of r, g, and b must be between 0 and colormode.
|
|
|
|
If the turtle shape is a polygon, the interior of that polygon is
|
|
drawn with the newly set fillcolor.
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.fillcolor("violet")
|
|
>>> turtle.fillcolor()
|
|
'violet'
|
|
>>> colormode(255)
|
|
>>> turtle.fillcolor("#ffffff")
|
|
>>> turtle.fillcolor()
|
|
(255.0, 255.0, 255.0)
|
|
"""
|
|
if args:
|
|
color = self._colorstr(args)
|
|
if color == self._fillcolor:
|
|
return
|
|
self.pen(fillcolor=color)
|
|
else:
|
|
return self._color(self._fillcolor)
|
|
|
|
def teleport(self, x=None, y=None, *, fill_gap: bool = False) -> None:
|
|
"""To be overwritten by the child class RawTurtle.
|
|
|
|
Includes no TNavigator references.
|
|
"""
|
|
pendown = self.isdown()
|
|
if pendown:
|
|
self.pen(pendown=False)
|
|
self.pen(pendown=pendown)
|
|
|
|
def showturtle(self):
|
|
"""Make the turtle visible.
|
|
|
|
Aliases: showturtle | st
|
|
"""
|
|
self.pen(shown=True)
|
|
|
|
def hideturtle(self):
|
|
"""Make the turtle invisible.
|
|
|
|
Aliases: hideturtle | ht
|
|
|
|
If the pen is down, movement still draws.
|
|
"""
|
|
self.pen(shown=False)
|
|
|
|
def isvisible(self):
|
|
"""Return True if the turtle is visible, False if it's hidden.
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.hideturtle()
|
|
>>> turtle.isvisible()
|
|
False
|
|
>>> turtle.showturtle()
|
|
>>> turtle.isvisible()
|
|
True
|
|
"""
|
|
return self._shown
|
|
|
|
def pen(self, pen=None, **pendict):
|
|
"""Return or set the pen's attributes.
|
|
|
|
Arguments:
|
|
pen -- a dictionary with some or all of the below listed keys.
|
|
**pendict -- one or more keyword-arguments with the below
|
|
listed keys as keywords.
|
|
|
|
Return or set the pen's attributes in a 'pen-dictionary'
|
|
with the following key/value pairs:
|
|
"shown" : True/False
|
|
"pendown" : True/False
|
|
"pencolor" : color-string or color-tuple
|
|
"fillcolor" : color-string or color-tuple
|
|
"pensize" : positive number
|
|
"speed" : number in range 0..10
|
|
"resizemode" : "auto" or "user" or "noresize"
|
|
"stretchfactor": (positive number, positive number)
|
|
"shearfactor": number
|
|
"outline" : positive number
|
|
"tilt" : number
|
|
|
|
This dictionary can be used as argument for a subsequent
|
|
pen()-call to restore the former pen-state. Moreover one
|
|
or more of these attributes can be provided as keyword-arguments.
|
|
This can be used to set several pen attributes in one statement.
|
|
|
|
|
|
Examples (for a Turtle instance named turtle):
|
|
>>> turtle.pen(fillcolor="black", pencolor="red", pensize=10)
|
|
>>> turtle.pen()
|
|
{'pensize': 10, 'shown': True, 'resizemode': 'auto', 'outline': 1,
|
|
'pencolor': 'red', 'pendown': True, 'fillcolor': 'black',
|
|
'stretchfactor': (1,1), 'speed': 3, 'shearfactor': 0.0}
|
|
>>> penstate=turtle.pen()
|
|
>>> turtle.color("yellow","")
|
|
>>> turtle.penup()
|
|
>>> turtle.pen()
|
|
{'pensize': 10, 'shown': True, 'resizemode': 'auto', 'outline': 1,
|
|
'pencolor': 'yellow', 'pendown': False, 'fillcolor': '',
|
|
'stretchfactor': (1,1), 'speed': 3, 'shearfactor': 0.0}
|
|
>>> p.pen(penstate, fillcolor="green")
|
|
>>> p.pen()
|
|
{'pensize': 10, 'shown': True, 'resizemode': 'auto', 'outline': 1,
|
|
'pencolor': 'red', 'pendown': True, 'fillcolor': 'green',
|
|
'stretchfactor': (1,1), 'speed': 3, 'shearfactor': 0.0}
|
|
"""
|
|
_pd = {"shown" : self._shown,
|
|
"pendown" : self._drawing,
|
|
"pencolor" : self._pencolor,
|
|
"fillcolor" : self._fillcolor,
|
|
"pensize" : self._pensize,
|
|
"speed" : self._speed,
|
|
"resizemode" : self._resizemode,
|
|
"stretchfactor" : self._stretchfactor,
|
|
"shearfactor" : self._shearfactor,
|
|
"outline" : self._outlinewidth,
|
|
"tilt" : self._tilt
|
|
}
|
|
|
|
if not (pen or pendict):
|
|
return _pd
|
|
|
|
if isinstance(pen, dict):
|
|
p = pen
|
|
else:
|
|
p = {}
|
|
p.update(pendict)
|
|
|
|
_p_buf = {}
|
|
for key in p:
|
|
_p_buf[key] = _pd[key]
|
|
|
|
if self.undobuffer:
|
|
self.undobuffer.push(("pen", _p_buf))
|
|
|
|
newLine = False
|
|
if "pendown" in p:
|
|
if self._drawing != p["pendown"]:
|
|
newLine = True
|
|
if "pencolor" in p:
|
|
if isinstance(p["pencolor"], tuple):
|
|
p["pencolor"] = self._colorstr((p["pencolor"],))
|
|
if self._pencolor != p["pencolor"]:
|
|
newLine = True
|
|
if "pensize" in p:
|
|
if self._pensize != p["pensize"]:
|
|
newLine = True
|
|
if newLine:
|
|
self._newLine()
|
|
if "pendown" in p:
|
|
self._drawing = p["pendown"]
|
|
if "pencolor" in p:
|
|
self._pencolor = p["pencolor"]
|
|
if "pensize" in p:
|
|
self._pensize = p["pensize"]
|
|
if "fillcolor" in p:
|
|
if isinstance(p["fillcolor"], tuple):
|
|
p["fillcolor"] = self._colorstr((p["fillcolor"],))
|
|
self._fillcolor = p["fillcolor"]
|
|
if "speed" in p:
|
|
self._speed = p["speed"]
|
|
if "resizemode" in p:
|
|
self._resizemode = p["resizemode"]
|
|
if "stretchfactor" in p:
|
|
sf = p["stretchfactor"]
|
|
if isinstance(sf, (int, float)):
|
|
sf = (sf, sf)
|
|
self._stretchfactor = sf
|
|
if "shearfactor" in p:
|
|
self._shearfactor = p["shearfactor"]
|
|
if "outline" in p:
|
|
self._outlinewidth = p["outline"]
|
|
if "shown" in p:
|
|
self._shown = p["shown"]
|
|
if "tilt" in p:
|
|
self._tilt = p["tilt"]
|
|
if "stretchfactor" in p or "tilt" in p or "shearfactor" in p:
|
|
scx, scy = self._stretchfactor
|
|
shf = self._shearfactor
|
|
sa, ca = math.sin(self._tilt), math.cos(self._tilt)
|
|
self._shapetrafo = ( scx*ca, scy*(shf*ca + sa),
|
|
-scx*sa, scy*(ca - shf*sa))
|
|
self._update()
|
|
|
|
# Three dummy methods to be implemented by the child class:
|
|
|
|
def _newLine(self, usePos = True):
|
|
"""Dummy method - to be overwritten by the child class."""
|
|
def _update(self, count=True, forced=False):
|
|
"""Dummy method - to be overwritten by the child class."""
|
|
def _color(self, args):
|
|
"""Dummy method - to be overwritten by the child class."""
|
|
def _colorstr(self, args):
|
|
"""Dummy method - to be overwritten by the child class."""
|
|
|
|
# Aliases for the commonly used commands:
|
|
|
|
width = pensize
|
|
up = penup
|
|
pu = penup
|
|
pd = pendown
|
|
down = pendown
|
|
st = showturtle
|
|
ht = hideturtle
|
|
|
|
|
|
class _TurtleImage:
|
|
"""Helper class holding the canvas items that display a turtle's shape."""
|
|
|
|
def __init__(self, screen, shapeIndex):
|
|
self.screen = screen
|
|
self._type = None
|
|
self._setshape(shapeIndex)
|
|
|
|
def _setshape(self, shapeIndex):
|
|
screen = self.screen
|
|
self.shapeIndex = shapeIndex
|
|
if self._type == "polygon" == screen._shapes[shapeIndex]._type:
|
|
return
|
|
if self._type == "image" == screen._shapes[shapeIndex]._type:
|
|
return
|
|
if self._type in ["image", "polygon"]:
|
|
screen._delete(self._item)
|
|
elif self._type == "compound":
|
|
for item in self._item:
|
|
screen._delete(item)
|
|
self._type = screen._shapes[shapeIndex]._type
|
|
if self._type == "polygon":
|
|
self._item = screen._createpoly()
|
|
elif self._type == "image":
|
|
self._item = screen._createimage(screen._shapes["blank"]._data)
|
|
elif self._type == "compound":
|
|
self._item = [screen._createpoly() for item in
|
|
screen._shapes[shapeIndex]._data]
|
|
|
|
|
|
class RawTurtle(TPen, TNavigator):
|
|
"""A turtle that draws on the given canvas or TurtleScreen.
|
|
|
|
Combines the navigation part (TNavigator) and the drawing part (TPen)
|
|
and adds the animation of the turtle on the screen.
|
|
"""
|
|
screens = []
|
|
|
|
def __init__(self, canvas=None,
|
|
shape=_CFG["shape"],
|
|
undobuffersize=_CFG["undobuffersize"],
|
|
visible=_CFG["visible"]):
|
|
if isinstance(canvas, _Screen):
|
|
self.screen = canvas
|
|
elif isinstance(canvas, TurtleScreen):
|
|
if canvas not in RawTurtle.screens:
|
|
RawTurtle.screens.append(canvas)
|
|
self.screen = canvas
|
|
elif isinstance(canvas, (ScrolledCanvas, Canvas)):
|
|
for screen in RawTurtle.screens:
|
|
if screen.cv == canvas:
|
|
self.screen = screen
|
|
break
|
|
else:
|
|
self.screen = TurtleScreen(canvas)
|
|
RawTurtle.screens.append(self.screen)
|
|
else:
|
|
raise TurtleGraphicsError("bad canvas argument %s" % canvas)
|
|
|
|
screen = self.screen
|
|
TNavigator.__init__(self, screen.mode())
|
|
TPen.__init__(self)
|
|
screen._turtles.append(self)
|
|
self.drawingLineItem = screen._createline()
|
|
self.turtle = _TurtleImage(screen, shape)
|
|
self._poly = None
|
|
self._creatingPoly = False
|
|
self._fillitem = self._fillpath = None
|
|
self._shown = visible
|
|
self._hidden_from_screen = False
|
|
self.currentLineItem = screen._createline()
|
|
self.currentLine = [self._position]
|
|
self.items = [self.currentLineItem]
|
|
self.stampItems = []
|
|
self._undobuffersize = undobuffersize
|
|
self.undobuffer = Tbuffer(undobuffersize)
|
|
self._update()
|
|
|
|
def reset(self):
|
|
"""Delete the turtle's drawings and re-center the turtle.
|
|
|
|
The turtle's settings are restored to their default values.
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.goto(0,-22)
|
|
>>> turtle.left(100)
|
|
>>> turtle.position()
|
|
(0.00,-22.00)
|
|
>>> turtle.heading()
|
|
100.0
|
|
>>> turtle.reset()
|
|
>>> turtle.position()
|
|
(0.00,0.00)
|
|
>>> turtle.heading()
|
|
0.0
|
|
"""
|
|
TNavigator.reset(self)
|
|
TPen._reset(self)
|
|
self._clear()
|
|
self._drawturtle()
|
|
self._update()
|
|
|
|
def setundobuffer(self, size):
|
|
"""Set or disable the undo buffer.
|
|
|
|
If size is an integer, an empty undo buffer of the given size is
|
|
installed. size is the maximum number of turtle actions that can
|
|
be undone by undo(). If size is None, the undo buffer is disabled.
|
|
|
|
Argument:
|
|
size -- an integer or None
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.setundobuffer(42)
|
|
"""
|
|
if size is None or size <= 0:
|
|
self.undobuffer = None
|
|
else:
|
|
self.undobuffer = Tbuffer(size)
|
|
|
|
def undobufferentries(self):
|
|
"""Return the number of entries in the undo buffer.
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> while undobufferentries():
|
|
... undo()
|
|
"""
|
|
if self.undobuffer is None:
|
|
return 0
|
|
return self.undobuffer.nr_of_items()
|
|
|
|
def _clear(self):
|
|
"""Delete all of the pen's drawings."""
|
|
self._fillitem = self._fillpath = None
|
|
for item in self.items:
|
|
self.screen._delete(item)
|
|
self.currentLineItem = self.screen._createline()
|
|
self.currentLine = []
|
|
if self._drawing:
|
|
self.currentLine.append(self._position)
|
|
self.items = [self.currentLineItem]
|
|
self.clearstamps()
|
|
self.setundobuffer(self._undobuffersize)
|
|
|
|
|
|
def clear(self):
|
|
"""Delete the turtle's drawings from the screen.
|
|
|
|
The state and position of the turtle as well as the drawings of
|
|
other turtles are not affected.
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.clear()
|
|
"""
|
|
self._clear()
|
|
self._update()
|
|
|
|
def _update_data(self):
|
|
self.screen._incrementudc()
|
|
if self.screen._updatecounter != 0:
|
|
return
|
|
if len(self.currentLine)>1:
|
|
self.screen._drawline(self.currentLineItem, self.currentLine,
|
|
self._pencolor, self._pensize)
|
|
|
|
def _update(self):
|
|
"""Perform a Turtle-data update."""
|
|
screen = self.screen
|
|
if screen._tracing == 0:
|
|
return
|
|
elif screen._tracing == 1:
|
|
self._update_data()
|
|
self._drawturtle()
|
|
screen._update() # TurtleScreenBase
|
|
screen._delay(screen._delayvalue) # TurtleScreenBase
|
|
else:
|
|
self._update_data()
|
|
if screen._updatecounter == 0:
|
|
for t in screen.turtles():
|
|
t._drawturtle()
|
|
screen._update()
|
|
|
|
def _tracer(self, flag=None, delay=None):
|
|
"""Forward flag and delay to the screen's tracer() method."""
|
|
return self.screen.tracer(flag, delay)
|
|
|
|
def _color(self, args):
|
|
return self.screen._color(args)
|
|
|
|
def _colorstr(self, args):
|
|
return self.screen._colorstr(args)
|
|
|
|
def _cc(self, args):
|
|
"""Convert colortriples to hexstrings."""
|
|
if isinstance(args, str):
|
|
return args
|
|
try:
|
|
r, g, b = args
|
|
except (TypeError, ValueError):
|
|
raise TurtleGraphicsError("bad color arguments: %s" % str(args))
|
|
if self.screen._colormode == 1.0:
|
|
r, g, b = [round(255.0*x) for x in (r, g, b)]
|
|
if not ((0 <= r <= 255) and (0 <= g <= 255) and (0 <= b <= 255)):
|
|
raise TurtleGraphicsError("bad color sequence: %s" % str(args))
|
|
return "#%02x%02x%02x" % (r, g, b)
|
|
|
|
def teleport(self, x=None, y=None, *, fill_gap: bool = False) -> None:
|
|
"""Teleport, or instantly move, the turtle to an absolute position.
|
|
|
|
Unlike goto(), no line is drawn. If x or y is not given, that
|
|
coordinate is kept as it is. The turtle's heading does not change.
|
|
|
|
If the turtle is filling a shape, the shape drawn so far is
|
|
finished and filled before teleporting, and filling starts again
|
|
at the new position. Pass fill_gap=True to keep filling one shape
|
|
instead. The teleport then counts as an edge of the shape, as it
|
|
would with goto().
|
|
|
|
Arguments:
|
|
x -- a number or None
|
|
y -- a number or None
|
|
fill_gap -- a boolean
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> tp = turtle.pos()
|
|
>>> tp
|
|
(0.00,0.00)
|
|
>>> turtle.teleport(60)
|
|
>>> turtle.pos()
|
|
(60.00,0.00)
|
|
>>> turtle.teleport(y=10)
|
|
>>> turtle.pos()
|
|
(60.00,10.00)
|
|
>>> turtle.teleport(20, 30)
|
|
>>> turtle.pos()
|
|
(20.00,30.00)
|
|
"""
|
|
pendown = self.isdown()
|
|
was_filling = self.filling()
|
|
with self._undo_sequence():
|
|
if pendown:
|
|
self.pen(pendown=False)
|
|
if was_filling and not fill_gap:
|
|
self.end_fill()
|
|
new_x = x if x is not None else self._position[0]
|
|
new_y = y if y is not None else self._position[1]
|
|
if self.undobuffer:
|
|
self.undobuffer.push(("teleport", self._position))
|
|
self._position = Vec2D(new_x, new_y)
|
|
self.pen(pendown=pendown)
|
|
if was_filling and not fill_gap:
|
|
self.begin_fill()
|
|
|
|
def clone(self):
|
|
"""Create and return a clone of the turtle.
|
|
|
|
The clone has the same position, heading and turtle properties.
|
|
|
|
Example:
|
|
>>> leonel = Turtle()
|
|
>>> marco = leonel.clone()
|
|
"""
|
|
screen = self.screen
|
|
self._newLine(self._drawing)
|
|
|
|
turtle = self.turtle
|
|
self.screen = None
|
|
self.turtle = None # too make self deepcopy-able
|
|
|
|
q = deepcopy(self)
|
|
|
|
self.screen = screen
|
|
self.turtle = turtle
|
|
|
|
q.screen = screen
|
|
q.turtle = _TurtleImage(screen, self.turtle.shapeIndex)
|
|
|
|
screen._turtles.append(q)
|
|
ttype = screen._shapes[self.turtle.shapeIndex]._type
|
|
if ttype == "polygon":
|
|
q.turtle._item = screen._createpoly()
|
|
elif ttype == "image":
|
|
q.turtle._item = screen._createimage(screen._shapes["blank"]._data)
|
|
elif ttype == "compound":
|
|
q.turtle._item = [screen._createpoly() for item in
|
|
screen._shapes[self.turtle.shapeIndex]._data]
|
|
q.currentLineItem = screen._createline()
|
|
q._update()
|
|
return q
|
|
|
|
def shape(self, name=None):
|
|
"""Set the turtle shape to the named shape.
|
|
|
|
If name is not given, return the name of the current shape. The
|
|
shape must exist in the TurtleScreen's shape dictionary. Initially
|
|
there are the following shapes: "arrow", "turtle", "circle",
|
|
"square", "triangle" and "classic". To add shapes, see the Screen
|
|
method register_shape().
|
|
|
|
Argument:
|
|
name -- a string or None
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.shape()
|
|
'classic'
|
|
>>> turtle.shape("turtle")
|
|
>>> turtle.shape()
|
|
'turtle'
|
|
"""
|
|
if name is None:
|
|
return self.turtle.shapeIndex
|
|
if not name in self.screen.getshapes():
|
|
raise TurtleGraphicsError("There is no shape named %s" % name)
|
|
self.turtle._setshape(name)
|
|
self._update()
|
|
|
|
def shapesize(self, stretch_wid=None, stretch_len=None, outline=None):
|
|
"""Return or set the turtle's stretch factors and outline width.
|
|
|
|
Aliases: shapesize | turtlesize
|
|
|
|
If no arguments are given, return the current values. Otherwise
|
|
set resizemode to "user", so that the turtle is displayed
|
|
stretched according to its stretch factors:
|
|
|
|
- stretch_wid: the stretch factor perpendicular to the turtle's
|
|
heading
|
|
- stretch_len: the stretch factor in the direction of the
|
|
turtle's heading
|
|
- outline: the width of the shape's outline
|
|
|
|
For example, shapesize(2, 1) makes the turtle twice as wide
|
|
without changing its length. If only stretch_wid is given,
|
|
stretch_len is set to the same value.
|
|
|
|
Arguments:
|
|
stretch_wid -- a positive number or None
|
|
stretch_len -- a positive number or None
|
|
outline -- a positive number or None
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.shapesize()
|
|
(1.0, 1.0, 1)
|
|
>>> turtle.resizemode("user")
|
|
>>> turtle.shapesize(5, 5, 12)
|
|
>>> turtle.shapesize()
|
|
(5, 5, 12)
|
|
>>> turtle.shapesize(outline=8)
|
|
>>> turtle.shapesize()
|
|
(5, 5, 8)
|
|
"""
|
|
if stretch_wid is stretch_len is outline is None:
|
|
stretch_wid, stretch_len = self._stretchfactor
|
|
return stretch_wid, stretch_len, self._outlinewidth
|
|
if stretch_wid == 0 or stretch_len == 0:
|
|
raise TurtleGraphicsError("stretch_wid/stretch_len must not be zero")
|
|
if stretch_wid is not None:
|
|
if stretch_len is None:
|
|
stretchfactor = stretch_wid, stretch_wid
|
|
else:
|
|
stretchfactor = stretch_wid, stretch_len
|
|
elif stretch_len is not None:
|
|
stretchfactor = self._stretchfactor[0], stretch_len
|
|
else:
|
|
stretchfactor = self._stretchfactor
|
|
if outline is None:
|
|
outline = self._outlinewidth
|
|
self.pen(resizemode="user",
|
|
stretchfactor=stretchfactor, outline=outline)
|
|
|
|
def shearfactor(self, shear=None):
|
|
"""Set or return the current shearfactor.
|
|
|
|
Optional argument: shear -- number, tangent of the shear angle
|
|
|
|
Shear the turtleshape according to the given shearfactor shear,
|
|
which is the tangent of the shear angle. DO NOT change the
|
|
turtle's heading (direction of movement).
|
|
If shear is not given: return the current shearfactor, i. e. the
|
|
tangent of the shear angle, by which lines parallel to the
|
|
heading of the turtle are sheared.
|
|
|
|
Examples (for a Turtle instance named turtle):
|
|
>>> turtle.shape("circle")
|
|
>>> turtle.shapesize(5,2)
|
|
>>> turtle.shearfactor(0.5)
|
|
>>> turtle.shearfactor()
|
|
>>> 0.5
|
|
"""
|
|
if shear is None:
|
|
return self._shearfactor
|
|
self.pen(resizemode="user", shearfactor=shear)
|
|
|
|
def tiltangle(self, angle=None):
|
|
"""Set or return the current tilt-angle.
|
|
|
|
Optional argument: angle -- number
|
|
|
|
Rotate the turtleshape to point in the direction specified by angle,
|
|
regardless of its current tilt-angle. DO NOT change the turtle's
|
|
heading (direction of movement).
|
|
If angle is not given: return the current tilt-angle, i. e. the angle
|
|
between the orientation of the turtleshape and the heading of the
|
|
turtle (its direction of movement).
|
|
|
|
Examples (for a Turtle instance named turtle):
|
|
>>> turtle.shape("circle")
|
|
>>> turtle.shapesize(5, 2)
|
|
>>> turtle.tiltangle()
|
|
0.0
|
|
>>> turtle.tiltangle(45)
|
|
>>> turtle.tiltangle()
|
|
45.0
|
|
>>> turtle.stamp()
|
|
>>> turtle.fd(50)
|
|
>>> turtle.tiltangle(-45)
|
|
>>> turtle.tiltangle()
|
|
315.0
|
|
>>> turtle.stamp()
|
|
>>> turtle.fd(50)
|
|
"""
|
|
if angle is None:
|
|
tilt = -math.degrees(self._tilt) * self._angleOrient
|
|
return (tilt / self._degreesPerAU) % self._fullcircle
|
|
else:
|
|
tilt = -angle * self._degreesPerAU * self._angleOrient
|
|
tilt = math.radians(tilt) % math.tau
|
|
self.pen(resizemode="user", tilt=tilt)
|
|
|
|
def tilt(self, angle):
|
|
"""Rotate the turtleshape by angle.
|
|
|
|
Argument:
|
|
angle - a number
|
|
|
|
Rotate the turtleshape by angle from its current tilt-angle,
|
|
but do NOT change the turtle's heading (direction of movement).
|
|
|
|
Examples (for a Turtle instance named turtle):
|
|
>>> turtle.shape("circle")
|
|
>>> turtle.shapesize(5,2)
|
|
>>> turtle.tilt(30)
|
|
>>> turtle.fd(50)
|
|
>>> turtle.tilt(30)
|
|
>>> turtle.fd(50)
|
|
"""
|
|
self.tiltangle(angle + self.tiltangle())
|
|
|
|
def shapetransform(self, t11=None, t12=None, t21=None, t22=None):
|
|
"""Set or return the current transformation matrix of the turtle shape.
|
|
|
|
Optional arguments: t11, t12, t21, t22 -- numbers.
|
|
|
|
If none of the matrix elements are given, return the transformation
|
|
matrix.
|
|
Otherwise set the given elements and transform the turtleshape
|
|
according to the matrix consisting of first row t11, t12 and
|
|
second row t21, 22.
|
|
Modify stretchfactor, shearfactor and tiltangle according to the
|
|
given matrix.
|
|
|
|
Examples (for a Turtle instance named turtle):
|
|
>>> turtle.shape("square")
|
|
>>> turtle.shapesize(4,2)
|
|
>>> turtle.shearfactor(-0.5)
|
|
>>> turtle.shapetransform()
|
|
(4.0, -1.0, -0.0, 2.0)
|
|
"""
|
|
if t11 is t12 is t21 is t22 is None:
|
|
return self._shapetrafo
|
|
m11, m12, m21, m22 = self._shapetrafo
|
|
if t11 is not None: m11 = t11
|
|
if t12 is not None: m12 = t12
|
|
if t21 is not None: m21 = t21
|
|
if t22 is not None: m22 = t22
|
|
if t11 * t22 - t12 * t21 == 0:
|
|
raise TurtleGraphicsError("Bad shape transform matrix: must not be singular")
|
|
self._shapetrafo = (m11, m12, m21, m22)
|
|
alfa = math.atan2(-m21, m11) % math.tau
|
|
sa, ca = math.sin(alfa), math.cos(alfa)
|
|
a11, a12, a21, a22 = (ca*m11 - sa*m21, ca*m12 - sa*m22,
|
|
sa*m11 + ca*m21, sa*m12 + ca*m22)
|
|
self._stretchfactor = a11, a22
|
|
self._shearfactor = a12/a22
|
|
self._tilt = alfa
|
|
self.pen(resizemode="user")
|
|
|
|
|
|
def _polytrafo(self, poly):
|
|
"""Computes transformed polygon shapes from a shape
|
|
according to current position and heading.
|
|
"""
|
|
screen = self.screen
|
|
p0, p1 = self._position
|
|
e0, e1 = self._orient
|
|
e = Vec2D(e0, e1 * screen.yscale / screen.xscale)
|
|
e0, e1 = (1.0 / abs(e)) * e
|
|
return [(p0+(e1*x+e0*y)/screen.xscale, p1+(-e0*x+e1*y)/screen.yscale)
|
|
for (x, y) in poly]
|
|
|
|
def get_shapepoly(self):
|
|
"""Return the current shape polygon as tuple of coordinate pairs.
|
|
|
|
No argument.
|
|
|
|
Examples (for a Turtle instance named turtle):
|
|
>>> turtle.shape("square")
|
|
>>> turtle.shapetransform(4, -1, 0, 2)
|
|
>>> turtle.get_shapepoly()
|
|
((50, -20), (30, 20), (-50, 20), (-30, -20))
|
|
|
|
"""
|
|
shape = self.screen._shapes[self.turtle.shapeIndex]
|
|
if shape._type == "polygon":
|
|
return self._getshapepoly(shape._data, shape._type == "compound")
|
|
# else return None
|
|
|
|
def _getshapepoly(self, polygon, compound=False):
|
|
"""Calculate transformed shape polygon according to resizemode
|
|
and shapetransform.
|
|
"""
|
|
if self._resizemode == "user" or compound:
|
|
t11, t12, t21, t22 = self._shapetrafo
|
|
elif self._resizemode == "auto":
|
|
l = max(1, self._pensize/5.0)
|
|
t11, t12, t21, t22 = l, 0, 0, l
|
|
elif self._resizemode == "noresize":
|
|
return polygon
|
|
return tuple((t11*x + t12*y, t21*x + t22*y) for (x, y) in polygon)
|
|
|
|
def _drawturtle(self):
|
|
"""Manages the correct rendering of the turtle with respect to
|
|
its shape, resizemode, stretch and tilt etc."""
|
|
screen = self.screen
|
|
shape = screen._shapes[self.turtle.shapeIndex]
|
|
ttype = shape._type
|
|
titem = self.turtle._item
|
|
if self._shown and screen._updatecounter == 0 and screen._tracing > 0:
|
|
self._hidden_from_screen = False
|
|
tshape = shape._data
|
|
if ttype == "polygon":
|
|
if self._resizemode == "noresize": w = 1
|
|
elif self._resizemode == "auto": w = self._pensize
|
|
else: w =self._outlinewidth
|
|
shape = self._polytrafo(self._getshapepoly(tshape))
|
|
fc, oc = self._fillcolor, self._pencolor
|
|
screen._drawpoly(titem, shape, fill=fc, outline=oc,
|
|
width=w, top=True)
|
|
elif ttype == "image":
|
|
screen._drawimage(titem, self._position, tshape)
|
|
elif ttype == "compound":
|
|
for item, (poly, fc, oc) in zip(titem, tshape):
|
|
poly = self._polytrafo(self._getshapepoly(poly, True))
|
|
screen._drawpoly(item, poly, fill=self._cc(fc),
|
|
outline=self._cc(oc), width=self._outlinewidth, top=True)
|
|
else:
|
|
if self._hidden_from_screen:
|
|
return
|
|
if ttype == "polygon":
|
|
screen._drawpoly(titem, ((0, 0), (0, 0), (0, 0)), "", "")
|
|
elif ttype == "image":
|
|
screen._drawimage(titem, self._position,
|
|
screen._shapes["blank"]._data)
|
|
elif ttype == "compound":
|
|
for item in titem:
|
|
screen._drawpoly(item, ((0, 0), (0, 0), (0, 0)), "", "")
|
|
self._hidden_from_screen = True
|
|
|
|
############################## stamp stuff ###############################
|
|
|
|
def stamp(self):
|
|
"""Stamp a copy of the turtleshape onto the canvas and return its id.
|
|
|
|
No argument.
|
|
|
|
Stamp a copy of the turtle shape onto the canvas at the current
|
|
turtle position. Return a stamp_id for that stamp, which can be
|
|
used to delete it by calling clearstamp(stamp_id).
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.color("blue")
|
|
>>> turtle.stamp()
|
|
13
|
|
>>> turtle.fd(50)
|
|
"""
|
|
screen = self.screen
|
|
shape = screen._shapes[self.turtle.shapeIndex]
|
|
ttype = shape._type
|
|
tshape = shape._data
|
|
if ttype == "polygon":
|
|
stitem = screen._createpoly()
|
|
if self._resizemode == "noresize": w = 1
|
|
elif self._resizemode == "auto": w = self._pensize
|
|
else: w =self._outlinewidth
|
|
shape = self._polytrafo(self._getshapepoly(tshape))
|
|
fc, oc = self._fillcolor, self._pencolor
|
|
screen._drawpoly(stitem, shape, fill=fc, outline=oc,
|
|
width=w, top=True)
|
|
elif ttype == "image":
|
|
stitem = screen._createimage("")
|
|
screen._drawimage(stitem, self._position, tshape)
|
|
elif ttype == "compound":
|
|
stitem = []
|
|
for element in tshape:
|
|
item = screen._createpoly()
|
|
stitem.append(item)
|
|
stitem = tuple(stitem)
|
|
for item, (poly, fc, oc) in zip(stitem, tshape):
|
|
poly = self._polytrafo(self._getshapepoly(poly, True))
|
|
screen._drawpoly(item, poly, fill=self._cc(fc),
|
|
outline=self._cc(oc), width=self._outlinewidth, top=True)
|
|
self.stampItems.append(stitem)
|
|
if self.undobuffer:
|
|
self.undobuffer.push(("stamp", stitem))
|
|
return stitem
|
|
|
|
def _clearstamp(self, stampid):
|
|
"""Does the work for clearstamp() and clearstamps()."""
|
|
if stampid in self.stampItems:
|
|
if isinstance(stampid, tuple):
|
|
for subitem in stampid:
|
|
self.screen._delete(subitem)
|
|
else:
|
|
self.screen._delete(stampid)
|
|
self.stampItems.remove(stampid)
|
|
# Delete stampitem from undobuffer if necessary
|
|
# if clearstamp is called directly.
|
|
if self.undobuffer:
|
|
self.undobuffer.remove(("stamp", stampid))
|
|
|
|
def clearstamp(self, stampid):
|
|
"""Delete stamp with given stampid
|
|
|
|
Argument:
|
|
stampid - an integer, must be return value of previous stamp() call.
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.color("blue")
|
|
>>> astamp = turtle.stamp()
|
|
>>> turtle.fd(50)
|
|
>>> turtle.clearstamp(astamp)
|
|
"""
|
|
self._clearstamp(stampid)
|
|
self._update()
|
|
|
|
def clearstamps(self, n=None):
|
|
"""Delete all or first/last n of turtle's stamps.
|
|
|
|
Optional argument:
|
|
n -- an integer
|
|
|
|
If n is None, delete all of pen's stamps,
|
|
else if n > 0 delete first n stamps
|
|
else if n < 0 delete last n stamps.
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> for i in range(8):
|
|
... turtle.stamp(); turtle.fd(30)
|
|
...
|
|
>>> turtle.clearstamps(2)
|
|
>>> turtle.clearstamps(-2)
|
|
>>> turtle.clearstamps()
|
|
"""
|
|
if n is None:
|
|
toDelete = self.stampItems[:]
|
|
elif n >= 0:
|
|
toDelete = self.stampItems[:n]
|
|
else:
|
|
toDelete = self.stampItems[n:]
|
|
for item in toDelete:
|
|
self._clearstamp(item)
|
|
self._update()
|
|
|
|
def _goto(self, end):
|
|
"""Move the pen to the point end, thereby drawing a line
|
|
if pen is down. All other methods for turtle movement depend
|
|
on this one.
|
|
"""
|
|
## Version with undo-stuff
|
|
go_modes = ( self._drawing,
|
|
self._pencolor,
|
|
self._pensize,
|
|
isinstance(self._fillpath, list))
|
|
screen = self.screen
|
|
undo_entry = ("go", self._position, end, go_modes,
|
|
(self.currentLineItem,
|
|
self.currentLine[:],
|
|
screen._pointlist(self.currentLineItem),
|
|
self.items[:])
|
|
)
|
|
if self.undobuffer:
|
|
self.undobuffer.push(undo_entry)
|
|
start = self._position
|
|
if self._speed and screen._tracing == 1:
|
|
diff = (end-start)
|
|
diffsq = (diff[0]*screen.xscale)**2 + (diff[1]*screen.yscale)**2
|
|
nhops = 1+int((diffsq**0.5)/(3*(1.1**self._speed)*self._speed))
|
|
delta = diff * (1.0/nhops)
|
|
for n in range(1, nhops):
|
|
if n == 1:
|
|
top = True
|
|
else:
|
|
top = False
|
|
self._position = start + delta * n
|
|
if self._drawing:
|
|
screen._drawline(self.drawingLineItem,
|
|
(start, self._position),
|
|
self._pencolor, self._pensize, top)
|
|
self._update()
|
|
if self._drawing:
|
|
screen._drawline(self.drawingLineItem, ((0, 0), (0, 0)),
|
|
fill="", width=self._pensize)
|
|
# Turtle now at end,
|
|
if self._drawing: # now update currentLine
|
|
self.currentLine.append(end)
|
|
if isinstance(self._fillpath, list):
|
|
self._fillpath.append(end)
|
|
###### vererbung!!!!!!!!!!!!!!!!!!!!!!
|
|
self._position = end
|
|
if self._creatingPoly:
|
|
self._poly.append(end)
|
|
if len(self.currentLine) > 42: # 42! answer to the ultimate question
|
|
# of life, the universe and everything
|
|
self._newLine()
|
|
self._update() #count=True)
|
|
|
|
def _undogoto(self, entry):
|
|
"""Reverse a _goto(). Used for undo()."""
|
|
old, new, go_modes, coodata = entry
|
|
drawing, pc, ps, filling = go_modes
|
|
cLI, cL, pl, items = coodata
|
|
screen = self.screen
|
|
if abs(self._position - new) > 0.5:
|
|
print ("undogoto: HALLO-DA-STIMMT-WAS-NICHT!")
|
|
# restore former situation
|
|
self.currentLineItem = cLI
|
|
self.currentLine = cL
|
|
|
|
if pl == [(0, 0), (0, 0)]:
|
|
usepc = ""
|
|
else:
|
|
usepc = pc
|
|
screen._drawline(cLI, pl, fill=usepc, width=ps)
|
|
|
|
todelete = [i for i in self.items if (i not in items) and
|
|
(screen._type(i) == "line")]
|
|
for i in todelete:
|
|
screen._delete(i)
|
|
self.items.remove(i)
|
|
|
|
start = old
|
|
if self._speed and screen._tracing == 1:
|
|
diff = old - new
|
|
diffsq = (diff[0]*screen.xscale)**2 + (diff[1]*screen.yscale)**2
|
|
nhops = 1+int((diffsq**0.5)/(3*(1.1**self._speed)*self._speed))
|
|
delta = diff * (1.0/nhops)
|
|
for n in range(1, nhops):
|
|
if n == 1:
|
|
top = True
|
|
else:
|
|
top = False
|
|
self._position = new + delta * n
|
|
if drawing:
|
|
screen._drawline(self.drawingLineItem,
|
|
(start, self._position),
|
|
pc, ps, top)
|
|
self._update()
|
|
if drawing:
|
|
screen._drawline(self.drawingLineItem, ((0, 0), (0, 0)),
|
|
fill="", width=ps)
|
|
# Turtle now at position old,
|
|
self._position = old
|
|
## if undo is done during creating a polygon, the last vertex
|
|
## will be deleted. if the polygon is entirely deleted,
|
|
## creatingPoly will be set to False.
|
|
## Polygons created before the last one will not be affected by undo()
|
|
if self._creatingPoly:
|
|
if len(self._poly) > 0:
|
|
self._poly.pop()
|
|
if self._poly == []:
|
|
self._creatingPoly = False
|
|
self._poly = None
|
|
if filling:
|
|
if self._fillpath == []:
|
|
self._fillpath = None
|
|
print("Unwahrscheinlich in _undogoto!")
|
|
elif self._fillpath is not None:
|
|
self._fillpath.pop()
|
|
self._update() #count=True)
|
|
|
|
def _rotate(self, angle):
|
|
"""Turns pen clockwise by angle."""
|
|
if self.undobuffer:
|
|
self.undobuffer.push(("rot", angle, self._degreesPerAU))
|
|
angle *= self._degreesPerAU
|
|
neworient = self._orient.rotate(angle)
|
|
tracing = self.screen._tracing
|
|
if tracing == 1 and self._speed > 0:
|
|
anglevel = 3.0 * self._speed
|
|
steps = 1 + int(abs(angle)/anglevel)
|
|
delta = 1.0*angle/steps
|
|
for _ in range(steps):
|
|
self._orient = self._orient.rotate(delta)
|
|
self._update()
|
|
self._orient = neworient
|
|
self._update()
|
|
|
|
def _newLine(self, usePos=True):
|
|
"""Closes the current line item and starts a new one.
|
|
|
|
If the current line becomes too long, animation performance (via _drawline)
|
|
can slow down considerably.
|
|
"""
|
|
if len(self.currentLine) > 1:
|
|
self.screen._drawline(self.currentLineItem, self.currentLine,
|
|
self._pencolor, self._pensize)
|
|
self.currentLineItem = self.screen._createline()
|
|
self.items.append(self.currentLineItem)
|
|
else:
|
|
self.screen._drawline(self.currentLineItem, top=True)
|
|
self.currentLine = []
|
|
if usePos:
|
|
self.currentLine = [self._position]
|
|
|
|
def filling(self):
|
|
"""Return True if the turtle is filling, False otherwise.
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.begin_fill()
|
|
>>> if turtle.filling():
|
|
... turtle.pensize(5)
|
|
... else:
|
|
... turtle.pensize(3)
|
|
"""
|
|
return isinstance(self._fillpath, list)
|
|
|
|
@contextmanager
|
|
def fill(self):
|
|
"""A context manager for filling a shape.
|
|
|
|
Implicitly ensures the code block is wrapped with
|
|
begin_fill() and end_fill().
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.color("black", "red")
|
|
>>> with turtle.fill():
|
|
... turtle.circle(60)
|
|
"""
|
|
self.begin_fill()
|
|
try:
|
|
yield
|
|
finally:
|
|
self.end_fill()
|
|
|
|
def begin_fill(self):
|
|
"""Called just before drawing a shape to be filled.
|
|
|
|
No argument.
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.color("black", "red")
|
|
>>> turtle.begin_fill()
|
|
>>> turtle.circle(60)
|
|
>>> turtle.end_fill()
|
|
"""
|
|
if not self.filling():
|
|
self._fillitem = self.screen._createpoly()
|
|
self.items.append(self._fillitem)
|
|
self._fillpath = [self._position]
|
|
self._newLine()
|
|
if self.undobuffer:
|
|
self.undobuffer.push(("beginfill", self._fillitem))
|
|
self._update()
|
|
|
|
def end_fill(self):
|
|
"""Fill the shape drawn after the call begin_fill().
|
|
|
|
No argument.
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.color("black", "red")
|
|
>>> turtle.begin_fill()
|
|
>>> turtle.circle(60)
|
|
>>> turtle.end_fill()
|
|
"""
|
|
if self.filling():
|
|
if len(self._fillpath) > 2:
|
|
self.screen._drawpoly(self._fillitem, self._fillpath,
|
|
fill=self._fillcolor)
|
|
if self.undobuffer:
|
|
self.undobuffer.push(("dofill", self._fillitem))
|
|
self._fillitem = self._fillpath = None
|
|
self._update()
|
|
|
|
def dot(self, size=None, *color):
|
|
"""Draw a circular dot with diameter size, using color.
|
|
|
|
If size is not given, the larger of pensize + 4 and 2 * pensize is
|
|
used. If color is not given, the current pencolor is used.
|
|
|
|
Arguments:
|
|
size -- a positive integer or None
|
|
color -- a colorstring or a numeric color tuple
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.home()
|
|
>>> turtle.dot()
|
|
>>> turtle.fd(50); turtle.dot(20, "blue"); turtle.fd(50)
|
|
"""
|
|
if not color:
|
|
if isinstance(size, (str, tuple)):
|
|
color = self._colorstr(size)
|
|
size = self._pensize + max(self._pensize, 4)
|
|
else:
|
|
color = self._pencolor
|
|
if not size:
|
|
size = self._pensize + max(self._pensize, 4)
|
|
else:
|
|
if size is None:
|
|
size = self._pensize + max(self._pensize, 4)
|
|
color = self._colorstr(color)
|
|
# If screen were to gain a dot function, see GH #104218.
|
|
pen = self.pen()
|
|
with self._undo_sequence():
|
|
try:
|
|
if self.resizemode() == 'auto':
|
|
self.ht()
|
|
self.pendown()
|
|
self.pensize(size)
|
|
self.pencolor(color)
|
|
self.forward(0)
|
|
finally:
|
|
self.pen(pen)
|
|
|
|
def _write(self, txt, align, font):
|
|
"""Performs the writing for write()
|
|
"""
|
|
item, end = self.screen._write(self._position, txt, align, font,
|
|
self._pencolor)
|
|
self._update()
|
|
self.items.append(item)
|
|
if self.undobuffer:
|
|
self.undobuffer.push(("wri", item))
|
|
return end
|
|
|
|
def write(self, arg, move=False, align="left", font=("Arial", 8, "normal")):
|
|
"""Write text at the current turtle position.
|
|
|
|
Arguments:
|
|
arg -- info, which is to be written to the TurtleScreen
|
|
move (optional) -- True/False
|
|
align (optional) -- one of the strings "left", "center" or right"
|
|
font (optional) -- a triple (fontname, fontsize, fonttype)
|
|
|
|
Write text - the string representation of arg - at the current
|
|
turtle position according to align ("left", "center" or right")
|
|
and with the given font.
|
|
If move is True, the pen is moved to the bottom-right corner
|
|
of the text. By default, move is False.
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.write('Home = ', True, align="center")
|
|
>>> turtle.write((0,0), True)
|
|
"""
|
|
with self._undo_sequence():
|
|
end = self._write(str(arg), align.lower(), font)
|
|
if move:
|
|
x, y = self.pos()
|
|
self.setpos(end, y)
|
|
|
|
@contextmanager
|
|
def poly(self):
|
|
"""A context manager for recording the vertices of a polygon.
|
|
|
|
Implicitly ensures that the code block is wrapped with
|
|
begin_poly() and end_poly()
|
|
|
|
Example (for a Turtle instance named turtle) where we create a
|
|
triangle as the polygon and move the turtle 100 steps forward:
|
|
>>> with turtle.poly():
|
|
... for side in range(3)
|
|
... turtle.forward(50)
|
|
... turtle.right(60)
|
|
>>> turtle.forward(100)
|
|
"""
|
|
self.begin_poly()
|
|
try:
|
|
yield
|
|
finally:
|
|
self.end_poly()
|
|
|
|
def begin_poly(self):
|
|
"""Start recording the vertices of a polygon.
|
|
|
|
The current turtle position is the first vertex of the polygon.
|
|
"""
|
|
self._poly = [self._position]
|
|
self._creatingPoly = True
|
|
|
|
def end_poly(self):
|
|
"""Stop recording the vertices of a polygon.
|
|
|
|
The current turtle position is the last vertex of the polygon.
|
|
This will be connected with the first vertex.
|
|
"""
|
|
self._creatingPoly = False
|
|
|
|
def get_poly(self):
|
|
"""Return the last recorded polygon.
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.home()
|
|
>>> turtle.begin_poly()
|
|
>>> turtle.fd(100)
|
|
>>> turtle.left(20)
|
|
>>> turtle.fd(30)
|
|
>>> turtle.left(60)
|
|
>>> turtle.fd(50)
|
|
>>> turtle.end_poly()
|
|
>>> p = turtle.get_poly()
|
|
>>> register_shape("myFavouriteShape", p)
|
|
"""
|
|
# Check if there are any poly?
|
|
if self._poly is not None:
|
|
return tuple(self._poly)
|
|
|
|
def getscreen(self):
|
|
"""Return the TurtleScreen object the turtle is drawing on.
|
|
|
|
So TurtleScreen-methods can be called for that object.
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> ts = turtle.getscreen()
|
|
>>> ts
|
|
<turtle.TurtleScreen object at ...>
|
|
>>> ts.bgcolor("pink")
|
|
"""
|
|
return self.screen
|
|
|
|
def getturtle(self):
|
|
"""Return the Turtle object itself.
|
|
|
|
Aliases: getturtle | getpen
|
|
|
|
Call getturtle() to get the turtle used by functions such as
|
|
forward() and left().
|
|
|
|
Example:
|
|
>>> pet = getturtle()
|
|
>>> pet.fd(50)
|
|
>>> pet
|
|
<turtle.Turtle object at 0x...>
|
|
"""
|
|
return self
|
|
|
|
getpen = getturtle
|
|
|
|
|
|
################################################################
|
|
### screen oriented methods recurring to methods of TurtleScreen
|
|
################################################################
|
|
|
|
def _delay(self, delay=None):
|
|
"""Set the delay which determines the speed of turtle animation."""
|
|
return self.screen.delay(delay)
|
|
|
|
def onclick(self, fun, btn=1, add=None):
|
|
"""Bind fun to mouse-click events on this turtle.
|
|
|
|
fun is called with the coordinates of the clicked point on the
|
|
canvas. If fun is None, existing bindings are removed. btn is the
|
|
number of the mouse button and defaults to 1, the left mouse
|
|
button. If add is True, a new binding is added, otherwise it
|
|
replaces a former binding.
|
|
|
|
Arguments:
|
|
fun -- a function with two arguments or None
|
|
btn -- an integer
|
|
add -- a boolean or None
|
|
|
|
Example for the anonymous turtle, that is, the procedural way:
|
|
>>> def turn(x, y):
|
|
... left(180)
|
|
...
|
|
>>> onclick(turn) # Now clicking into the turtle will turn it.
|
|
>>> onclick(None) # event-binding will be removed
|
|
"""
|
|
self.screen._onclick(self.turtle._item, fun, btn, add)
|
|
self._update()
|
|
|
|
def onrelease(self, fun, btn=1, add=None):
|
|
"""Bind fun to mouse-button-release event on this turtle on canvas.
|
|
|
|
Arguments:
|
|
fun -- a function with two arguments, to which will be assigned
|
|
the coordinates of the clicked point on the canvas.
|
|
btn -- number of the mouse-button defaults to 1 (left mouse button).
|
|
|
|
Example (for a MyTurtle instance named joe):
|
|
>>> class MyTurtle(Turtle):
|
|
... def glow(self,x,y):
|
|
... self.fillcolor("red")
|
|
... def unglow(self,x,y):
|
|
... self.fillcolor("")
|
|
...
|
|
>>> joe = MyTurtle()
|
|
>>> joe.onclick(joe.glow)
|
|
>>> joe.onrelease(joe.unglow)
|
|
|
|
Clicking on joe turns fillcolor red, unclicking turns it to
|
|
transparent.
|
|
"""
|
|
self.screen._onrelease(self.turtle._item, fun, btn, add)
|
|
self._update()
|
|
|
|
def ondrag(self, fun, btn=1, add=None):
|
|
"""Bind fun to mouse-move event on this turtle on canvas.
|
|
|
|
Arguments:
|
|
fun -- a function with two arguments, to which will be assigned
|
|
the coordinates of the clicked point on the canvas.
|
|
btn -- number of the mouse-button defaults to 1 (left mouse button).
|
|
|
|
Every sequence of mouse-move-events on a turtle is preceded by a
|
|
mouse-click event on that turtle.
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> turtle.ondrag(turtle.goto)
|
|
|
|
Subsequently clicking and dragging a Turtle will move it
|
|
across the screen thereby producing handdrawings (if pen is
|
|
down).
|
|
"""
|
|
self.screen._ondrag(self.turtle._item, fun, btn, add)
|
|
|
|
|
|
def _undo(self, action, data):
|
|
"""Does the main part of the work for undo()."""
|
|
if self.undobuffer is None:
|
|
return
|
|
if action == "rot":
|
|
angle, degPAU = data
|
|
self._rotate(-angle*degPAU/self._degreesPerAU)
|
|
self.undobuffer.pop()
|
|
elif action == "stamp":
|
|
stitem = data[0]
|
|
self.clearstamp(stitem)
|
|
elif action == "go":
|
|
self._undogoto(data)
|
|
elif action == "teleport":
|
|
self._position = data[0]
|
|
self._update()
|
|
elif action in ["wri", "dot"]:
|
|
item = data[0]
|
|
self.screen._delete(item)
|
|
self.items.remove(item)
|
|
elif action == "dofill":
|
|
item = data[0]
|
|
self.screen._drawpoly(item, ((0, 0),(0, 0),(0, 0)),
|
|
fill="", outline="")
|
|
elif action == "beginfill":
|
|
item = data[0]
|
|
self._fillitem = self._fillpath = None
|
|
if item in self.items:
|
|
self.screen._delete(item)
|
|
self.items.remove(item)
|
|
elif action == "pen":
|
|
TPen.pen(self, data[0])
|
|
self.undobuffer.pop()
|
|
|
|
def undo(self):
|
|
"""undo (repeatedly) the last turtle action.
|
|
|
|
No argument.
|
|
|
|
undo (repeatedly) the last turtle action.
|
|
Number of available undo actions is determined by the size of
|
|
the undobuffer.
|
|
|
|
Example (for a Turtle instance named turtle):
|
|
>>> for i in range(4):
|
|
... turtle.fd(50); turtle.lt(80)
|
|
...
|
|
>>> for i in range(8):
|
|
... turtle.undo()
|
|
...
|
|
"""
|
|
if self.undobuffer is None:
|
|
return
|
|
item = self.undobuffer.pop()
|
|
action = item[0]
|
|
data = item[1:]
|
|
if action == "seq":
|
|
while data:
|
|
item = data.pop()
|
|
self._undo(item[0], item[1:])
|
|
else:
|
|
self._undo(action, data)
|
|
|
|
turtlesize = shapesize
|
|
|
|
RawPen = RawTurtle
|
|
|
|
### Screen - Singleton ########################
|
|
|
|
def Screen():
|
|
"""Return the single screen object.
|
|
|
|
If it does not exist yet, create it. Otherwise return the existing one.
|
|
"""
|
|
if Turtle._screen is None:
|
|
Turtle._screen = _Screen()
|
|
return Turtle._screen
|
|
|
|
class _Screen(TurtleScreen):
|
|
|
|
_root = None
|
|
_canvas = None
|
|
_title = _CFG["title"]
|
|
|
|
def __init__(self):
|
|
if _Screen._root is None:
|
|
_Screen._root = self._root = _Root()
|
|
self._root.title(_Screen._title)
|
|
self._root.ondestroy(self._destroy)
|
|
if _Screen._canvas is None:
|
|
width = _CFG["width"]
|
|
height = _CFG["height"]
|
|
canvwidth = _CFG["canvwidth"]
|
|
canvheight = _CFG["canvheight"]
|
|
leftright = _CFG["leftright"]
|
|
topbottom = _CFG["topbottom"]
|
|
self._root.setupcanvas(width, height, canvwidth, canvheight)
|
|
_Screen._canvas = self._root._getcanvas()
|
|
TurtleScreen.__init__(self, _Screen._canvas)
|
|
self.setup(width, height, leftright, topbottom)
|
|
|
|
def setup(self, width=_CFG["width"], height=_CFG["height"],
|
|
startx=_CFG["leftright"], starty=_CFG["topbottom"]):
|
|
""" Set the size and position of the main window.
|
|
|
|
Arguments:
|
|
width: as integer a size in pixels, as float a fraction of the screen.
|
|
Default is 50% of screen.
|
|
height: as integer the height in pixels, as float a fraction of the
|
|
screen. Default is 75% of screen.
|
|
startx: if positive, starting position in pixels from the left
|
|
edge of the screen, if negative from the right edge
|
|
Default, startx=None is to center window horizontally.
|
|
starty: if positive, starting position in pixels from the top
|
|
edge of the screen, if negative from the bottom edge
|
|
Default, starty=None is to center window vertically.
|
|
|
|
Examples (for a Screen instance named screen):
|
|
>>> screen.setup (width=200, height=200, startx=0, starty=0)
|
|
|
|
sets window to 200x200 pixels, in upper left of screen
|
|
|
|
>>> screen.setup(width=.75, height=0.5, startx=None, starty=None)
|
|
|
|
sets window to 75% of screen by 50% of screen and centers
|
|
"""
|
|
if not hasattr(self._root, "set_geometry"):
|
|
return
|
|
sw = self._root.win_width()
|
|
sh = self._root.win_height()
|
|
if isinstance(width, float) and 0 <= width <= 1:
|
|
width = sw*width
|
|
if startx is None:
|
|
startx = (sw - width) / 2
|
|
if isinstance(height, float) and 0 <= height <= 1:
|
|
height = sh*height
|
|
if starty is None:
|
|
starty = (sh - height) / 2
|
|
self._root.set_geometry(width, height, startx, starty)
|
|
self.update()
|
|
|
|
def title(self, titlestring):
|
|
"""Set the title of the turtle window to titlestring.
|
|
|
|
The title is shown in the title bar of the turtle graphics window.
|
|
This is a method of the Screen class. It is not available for
|
|
TurtleScreen objects.
|
|
|
|
Argument:
|
|
titlestring -- a string
|
|
|
|
Example (for a Screen instance named screen):
|
|
>>> screen.title("Welcome to the turtle zoo!")
|
|
"""
|
|
if _Screen._root is not None:
|
|
_Screen._root.title(titlestring)
|
|
_Screen._title = titlestring
|
|
|
|
def _destroy(self):
|
|
root = self._root
|
|
if root is _Screen._root:
|
|
Turtle._pen = None
|
|
Turtle._screen = None
|
|
_Screen._root = None
|
|
_Screen._canvas = None
|
|
TurtleScreen._RUNNING = False
|
|
root.destroy()
|
|
|
|
def bye(self):
|
|
"""Close the turtle graphics window."""
|
|
self._destroy()
|
|
|
|
def exitonclick(self):
|
|
"""Go into mainloop until the mouse is clicked.
|
|
|
|
No arguments.
|
|
|
|
Bind bye() method to mouseclick on TurtleScreen.
|
|
If "using_IDLE" - value in configuration dictionary is False
|
|
(default value), enter mainloop.
|
|
If IDLE with -n switch (no subprocess) is used, this value should be
|
|
set to True in turtle.cfg. In this case IDLE's mainloop
|
|
is active also for the client script.
|
|
|
|
This is a method of the Screen-class and not available for
|
|
TurtleScreen instances.
|
|
|
|
Example (for a Screen instance named screen):
|
|
>>> screen.exitonclick()
|
|
|
|
"""
|
|
def exitGracefully(x, y):
|
|
"""Screen.bye() with two dummy-parameters"""
|
|
self.bye()
|
|
self.onclick(exitGracefully)
|
|
if _CFG["using_IDLE"]:
|
|
return
|
|
try:
|
|
mainloop()
|
|
except AttributeError:
|
|
exit(0)
|
|
|
|
class Turtle(RawTurtle):
|
|
"""RawTurtle auto-creating (scrolled) canvas.
|
|
|
|
When a Turtle object is created or a function derived from some
|
|
Turtle method is called a TurtleScreen object is automatically created.
|
|
"""
|
|
_pen = None
|
|
_screen = None
|
|
|
|
def __init__(self,
|
|
shape=_CFG["shape"],
|
|
undobuffersize=_CFG["undobuffersize"],
|
|
visible=_CFG["visible"]):
|
|
if Turtle._screen is None:
|
|
Turtle._screen = Screen()
|
|
RawTurtle.__init__(self, Turtle._screen,
|
|
shape=shape,
|
|
undobuffersize=undobuffersize,
|
|
visible=visible)
|
|
|
|
Pen = Turtle
|
|
|
|
def write_docstringdict(filename="turtle_docstringdict"):
|
|
"""Create and write docstring-dictionary to file.
|
|
|
|
Optional argument:
|
|
filename -- a string, used as filename
|
|
default value is turtle_docstringdict
|
|
|
|
Has to be called explicitly, (not used by the turtle-graphics classes)
|
|
The docstring dictionary will be written to the Python script <filename>.py
|
|
It is intended to serve as a template for translation of the docstrings
|
|
into different languages.
|
|
"""
|
|
docsdict = {}
|
|
|
|
for methodname in _tg_screen_functions:
|
|
key = "_Screen."+methodname
|
|
docsdict[key] = eval(key).__doc__
|
|
for methodname in _tg_turtle_functions:
|
|
key = "Turtle."+methodname
|
|
docsdict[key] = eval(key).__doc__
|
|
|
|
with open("%s.py" % filename,"w") as f:
|
|
keys = sorted(x for x in docsdict
|
|
if x.split('.')[1] not in _alias_list)
|
|
f.write('docsdict = {\n\n')
|
|
for key in keys[:-1]:
|
|
f.write('%s :\n' % repr(key))
|
|
f.write(' """%s\n""",\n\n' % docsdict[key])
|
|
key = keys[-1]
|
|
f.write('%s :\n' % repr(key))
|
|
f.write(' """%s\n"""\n\n' % docsdict[key])
|
|
f.write("}\n")
|
|
f.close()
|
|
|
|
def read_docstrings(lang):
|
|
"""Read in docstrings from a language-specific docstring dictionary.
|
|
|
|
Transfer docstrings, translated to lang, from a dictionary-file
|
|
to the methods of classes Screen and Turtle and - in revised form -
|
|
to the corresponding functions.
|
|
|
|
Entries naming a method which does not exist in this version are
|
|
ignored.
|
|
"""
|
|
module = __import__(f"turtle_docstringdict_{lang.lower()}")
|
|
docsdict = module.docsdict
|
|
for key in docsdict:
|
|
try:
|
|
# eval(key).im_func.__doc__ = docsdict[key]
|
|
eval(key).__doc__ = docsdict[key]
|
|
except AttributeError:
|
|
pass
|
|
except Exception:
|
|
print("Bad docstring-entry: %s" % key)
|
|
|
|
_LANGUAGE = _CFG["language"]
|
|
if not sys.flags.ignore_environment:
|
|
_LANGUAGE = environ.get("PYTHON_TURTLE_LANG") or _LANGUAGE
|
|
|
|
try:
|
|
if _LANGUAGE != "english":
|
|
read_docstrings(_LANGUAGE)
|
|
except ImportError:
|
|
print("Cannot find docsdict for", _LANGUAGE)
|
|
except Exception:
|
|
print ("Unknown Error when trying to import %s-docstring-dictionary" %
|
|
_LANGUAGE)
|
|
|
|
|
|
def getmethparlist(ob):
|
|
"""Get strings describing the arguments for the given object
|
|
|
|
Returns a pair of strings representing function parameter lists
|
|
including parenthesis. The first string is suitable for use in
|
|
function definition and the second is suitable for use in function
|
|
call. The "self" parameter is not included.
|
|
"""
|
|
orig_sig = inspect.signature(ob)
|
|
# bit of a hack for methods - turn it into a function
|
|
# but we drop the "self" param.
|
|
# Try and build one for Python defined functions
|
|
func_sig = orig_sig.replace(
|
|
parameters=list(orig_sig.parameters.values())[1:],
|
|
)
|
|
|
|
call_args = []
|
|
for param in func_sig.parameters.values():
|
|
match param.kind:
|
|
case (
|
|
inspect.Parameter.POSITIONAL_ONLY
|
|
| inspect.Parameter.POSITIONAL_OR_KEYWORD
|
|
):
|
|
call_args.append(param.name)
|
|
case inspect.Parameter.VAR_POSITIONAL:
|
|
call_args.append(f'*{param.name}')
|
|
case inspect.Parameter.KEYWORD_ONLY:
|
|
call_args.append(f'{param.name}={param.name}')
|
|
case inspect.Parameter.VAR_KEYWORD:
|
|
call_args.append(f'**{param.name}')
|
|
case _:
|
|
raise RuntimeError('Unsupported parameter kind', param.kind)
|
|
call_text = f'({', '.join(call_args)})'
|
|
|
|
return str(func_sig), call_text
|
|
|
|
def _turtle_docrevise(docstr):
|
|
"""To reduce docstrings from the RawTurtle class for functions."""
|
|
import re
|
|
if docstr is None:
|
|
return None
|
|
turtlename = _CFG["exampleturtle"]
|
|
newdocstr = docstr.replace("%s." % turtlename,"")
|
|
parexp = re.compile(r' \(.+ %s\):' % turtlename)
|
|
newdocstr = parexp.sub(":", newdocstr)
|
|
return newdocstr
|
|
|
|
def _screen_docrevise(docstr):
|
|
"""To reduce docstrings from the TurtleScreen class for functions."""
|
|
import re
|
|
if docstr is None:
|
|
return None
|
|
screenname = _CFG["examplescreen"]
|
|
newdocstr = docstr.replace("%s." % screenname,"")
|
|
parexp = re.compile(r' \(.+ %s\):' % screenname)
|
|
newdocstr = parexp.sub(":", newdocstr)
|
|
return newdocstr
|
|
|
|
## The following mechanism makes all methods of RawTurtle and Turtle available
|
|
## as functions. So we can enhance, change, add, delete methods to these
|
|
## classes and do not need to change anything here.
|
|
|
|
__func_body = """\
|
|
def {name}{paramslist}:
|
|
if {obj} is None:
|
|
if not TurtleScreen._RUNNING:
|
|
TurtleScreen._RUNNING = True
|
|
raise Terminator
|
|
{obj} = {init}
|
|
try:
|
|
return {obj}.{name}{argslist}
|
|
except TK.TclError:
|
|
if not TurtleScreen._RUNNING:
|
|
TurtleScreen._RUNNING = True
|
|
raise Terminator
|
|
raise
|
|
"""
|
|
|
|
def _make_global_funcs(functions, cls, obj, init, docrevise):
|
|
for methodname in functions:
|
|
method = getattr(cls, methodname)
|
|
pl1, pl2 = getmethparlist(method)
|
|
if pl1 == "":
|
|
print(">>>>>>", pl1, pl2)
|
|
continue
|
|
defstr = __func_body.format(obj=obj, init=init, name=methodname,
|
|
paramslist=pl1, argslist=pl2)
|
|
exec(defstr, globals())
|
|
globals()[methodname].__doc__ = docrevise(method.__doc__)
|
|
|
|
_make_global_funcs(_tg_screen_functions, _Screen,
|
|
'Turtle._screen', 'Screen()', _screen_docrevise)
|
|
_make_global_funcs(_tg_turtle_functions, Turtle,
|
|
'Turtle._pen', 'Turtle()', _turtle_docrevise)
|
|
|
|
|
|
done = mainloop
|
|
|
|
if __name__ == "__main__":
|
|
def switchpen():
|
|
if isdown():
|
|
pu()
|
|
else:
|
|
pd()
|
|
|
|
def demo1():
|
|
"""Demo of old turtle.py - module"""
|
|
reset()
|
|
tracer(True)
|
|
up()
|
|
backward(100)
|
|
down()
|
|
# draw 3 squares; the last filled
|
|
width(3)
|
|
for i in range(3):
|
|
if i == 2:
|
|
begin_fill()
|
|
for _ in range(4):
|
|
forward(20)
|
|
left(90)
|
|
if i == 2:
|
|
color("maroon")
|
|
end_fill()
|
|
up()
|
|
forward(30)
|
|
down()
|
|
width(1)
|
|
color("black")
|
|
# move out of the way
|
|
tracer(False)
|
|
up()
|
|
right(90)
|
|
forward(100)
|
|
right(90)
|
|
forward(100)
|
|
right(180)
|
|
down()
|
|
# some text
|
|
write("startstart", 1)
|
|
write("start", 1)
|
|
color("red")
|
|
# staircase
|
|
for i in range(5):
|
|
forward(20)
|
|
left(90)
|
|
forward(20)
|
|
right(90)
|
|
# filled staircase
|
|
tracer(True)
|
|
begin_fill()
|
|
for i in range(5):
|
|
forward(20)
|
|
left(90)
|
|
forward(20)
|
|
right(90)
|
|
end_fill()
|
|
# more text
|
|
|
|
def demo2():
|
|
"""Demo of some new features."""
|
|
speed(1)
|
|
st()
|
|
pensize(3)
|
|
setheading(towards(0, 0))
|
|
radius = distance(0, 0)/2.0
|
|
rt(90)
|
|
for _ in range(18):
|
|
switchpen()
|
|
circle(radius, 10)
|
|
write("wait a moment...")
|
|
while undobufferentries():
|
|
undo()
|
|
reset()
|
|
lt(90)
|
|
colormode(255)
|
|
laenge = 10
|
|
pencolor("green")
|
|
pensize(3)
|
|
lt(180)
|
|
for i in range(-2, 16):
|
|
if i > 0:
|
|
begin_fill()
|
|
fillcolor(255-15*i, 0, 15*i)
|
|
for _ in range(3):
|
|
fd(laenge)
|
|
lt(120)
|
|
end_fill()
|
|
laenge += 10
|
|
lt(15)
|
|
speed((speed()+1)%12)
|
|
#end_fill()
|
|
|
|
lt(120)
|
|
pu()
|
|
fd(70)
|
|
rt(30)
|
|
pd()
|
|
color("red","yellow")
|
|
speed(0)
|
|
begin_fill()
|
|
for _ in range(4):
|
|
circle(50, 90)
|
|
rt(90)
|
|
fd(30)
|
|
rt(90)
|
|
end_fill()
|
|
lt(90)
|
|
pu()
|
|
fd(30)
|
|
pd()
|
|
shape("turtle")
|
|
|
|
tri = getturtle()
|
|
tri.resizemode("auto")
|
|
turtle = Turtle()
|
|
turtle.resizemode("auto")
|
|
turtle.shape("turtle")
|
|
turtle.reset()
|
|
turtle.left(90)
|
|
turtle.speed(0)
|
|
turtle.up()
|
|
turtle.goto(280, 40)
|
|
turtle.lt(30)
|
|
turtle.down()
|
|
turtle.speed(6)
|
|
turtle.color("blue","orange")
|
|
turtle.pensize(2)
|
|
tri.speed(6)
|
|
setheading(towards(turtle))
|
|
count = 1
|
|
while tri.distance(turtle) > 4:
|
|
turtle.fd(3.5)
|
|
turtle.lt(0.6)
|
|
tri.setheading(tri.towards(turtle))
|
|
tri.fd(4)
|
|
if count % 20 == 0:
|
|
turtle.stamp()
|
|
tri.stamp()
|
|
switchpen()
|
|
count += 1
|
|
tri.write("CAUGHT! ", font=("Arial", 16, "bold"), align="right")
|
|
tri.pencolor("black")
|
|
tri.pencolor("red")
|
|
|
|
def baba(xdummy, ydummy):
|
|
clearscreen()
|
|
bye()
|
|
|
|
time.sleep(2)
|
|
|
|
while undobufferentries():
|
|
tri.undo()
|
|
turtle.undo()
|
|
tri.fd(50)
|
|
tri.write(" Click me!", font = ("Courier", 12, "bold") )
|
|
tri.onclick(baba, 1)
|
|
|
|
demo1()
|
|
demo2()
|
|
exitonclick()
|