Created
August 22, 2014 17:46
-
-
Save Frijol/71e1ebb71ef0ac3b58bb to your computer and use it in GitHub Desktop.
AutoFrost Cake Python GUI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Last modified 12/14/2010 | |
# | |
# Purpose: This code generates all parts of the cake creation GUI, | |
# converts user input to motor commands, and sends those commands to the arduino | |
from Tkinter import * | |
import sys | |
import serial | |
class Callable: | |
"""From the Python Cookbook 9.1, page 302""" | |
def __init__(self, func, *args, **kwds): | |
self.func = func | |
self.args = args | |
self.kwds = kwds | |
def __call__(self): | |
return apply(self.func, self.args, self.kwds) | |
def __str__(self): | |
return self.func.__name__ | |
def distance((x1, y1),(x2, y2)): | |
"""Calculates the distance between two points""" | |
dist = ((x2-x1)**2 + (y2-y1)**2)**.5 | |
return dist | |
# Below: filler definitions to enable drawing different nozzle shapes on the | |
# GUI. We never implemented this functionality, but the basic code structure | |
# is mostly here. | |
def create_smallstar(): | |
pass | |
def create_bigstar(): | |
pass | |
def create_diamond(): | |
pass | |
def create_circle(x, y, color, canvas): | |
x1, y1 = (x - 4), (y - 4) | |
x2, y2 = (x + 4), (y + 4) | |
canvas.create_oval(x1, y1, x2, y2, fill = color, outline = color) | |
def create_moon(): | |
pass | |
def create_toothedmoon(): | |
pass | |
### PaintGUI sets up a window based on the cake dimensions. A circular cake | |
### is shown by a circle in a black window, while a regular cake is just | |
### a rectangular canvas. Mycake is the object-oriented implemenatation. | |
class Mycake: | |
def __init__(self, size, color): | |
self.paintcolor = 'white' | |
self.nozzletype = 'circle' | |
self.cakecolor = color | |
self.pointslist = [] | |
self.currentlist = [] | |
self.size = size | |
def PaintGUI(self): | |
root = Tk() | |
root.title('Team Cake') | |
root.resizable(0,0) | |
# Sets up circular or rectangular canvas, scaled from the size the user | |
# has entered | |
if len(self.size) == 1: | |
diam = self.size[0] | |
side = diam*50 | |
canvas = Canvas(root, width=side, height=side, bg = 'black') | |
cake = canvas.create_oval(0,0,side,side,fill = self.cakecolor) | |
center = (side / 100.0, side / 100.0) | |
else: | |
canvas = Canvas(root, width = self.size[0]*50, | |
height = self.size[1]*50, | |
bg = self.cakecolor) | |
center =(self.size[0] / 100.0, self.size[1] / 100.0) | |
canvas.configure(cursor = 'crosshair') | |
# This is the paint-on-canvas function. When bound to a mouseclick | |
# event, it enables painting on the canvas. | |
def paint(event): | |
x = event.x | |
y = event.y | |
# Realsizepoint is the point on the canvas, scaled back into real | |
# inches. Listitem is the same information, plus the current color | |
# and nozzle type, for sending to the arduino. | |
realsizepoint = (event.x / 50.0, event.y / 50.0) | |
listitem = (event.x / 50.0, event.y / 50.0, | |
self.paintcolor, self.nozzletype) | |
def paintstuff(): | |
if len(self.currentlist) == 0: | |
# Here, is we created the other polygon-drawing functions | |
# and chose which one to call based on the current nozzle | |
# type, differently shaped nozzles could show up on the | |
# paint GUI | |
create_circle(x, y, self.paintcolor, canvas) | |
self.currentlist.append(listitem) | |
# Space out points appropriately -- to prevent the user's | |
# mouse speed from controlling frosting density, only | |
# place points at regular intervals | |
else: | |
oldx = self.currentlist[-1][0] | |
oldy = self.currentlist[-1][1] | |
if distance((oldx, oldy), realsizepoint) > .125: | |
# Again, we could generalize for other nozzle shapes | |
create_circle(x, y, self.paintcolor, canvas) | |
self.currentlist.append(listitem) | |
if len(self.size) == 2: | |
paintstuff() | |
# If the canvas is circular, only draw within the circle. | |
if len(self.size) == 1: | |
if distance(center, realsizepoint) < (self.size[0] / 2): | |
paintstuff() | |
# Using newlist will allow us to keep track of separate lines | |
# and stop frosting between shapes | |
def newlist(event): | |
self.pointslist.append(self.currentlist) | |
self.currentlist = [] | |
# If the user picks up his/her mouse, start a new list | |
canvas.bind('<ButtonRelease-1>', newlist) | |
# Clicking and moving the mouse while clicked draws on the canvas | |
canvas.bind("<B1-Motion>", paint) | |
canvas.bind('<Button-1>', paint) | |
# Set the paint color | |
def setcolor(color): | |
self.paintcolor = color | |
# Set the nozzle type | |
def setnozzle(shape): | |
self.nozzletype = shape | |
# Clear the entire canvas and delete all saved points from lists; redraw | |
# the circle for the cake if the cake is indeed circular | |
def clear(): | |
self.pointslist = [] | |
self.currentlist = [] | |
canvas.delete(ALL) | |
if len(self.size) == 1: | |
cake = canvas.create_oval(0,0,side,side,fill = self.cakecolor) | |
# Preview the frosted cake by running through the final list, converting | |
# from inches back to pixels, and drawing circles and lines on a preview | |
# canvas accordingly | |
def preview(): | |
donecake = Tk() | |
donecake.title('Team Cake') | |
donecake.resizable(0, 0) | |
if len(self.size) == 1: | |
diam = self.size[0] | |
side = diam*50 | |
palatte = Canvas(donecake, width=side, height=side, | |
bg = 'black') | |
cake = palatte.create_oval(0,0,side,side,fill = self.cakecolor) | |
else: | |
palatte = Canvas(donecake, width = self.size[0]*50, | |
height = self.size[1]*50, bg = self.cakecolor) | |
for linelist in self.pointslist: | |
palatte.create_oval(linelist[0][0] * 50 - 4, | |
linelist[0][1] * 50 - 4, | |
linelist[0][0] * 50 + 4, | |
linelist[0][1] * 50 + 4, | |
fill = linelist[0][2], | |
outline = linelist[0][2]) | |
for i in range(1, len(linelist)): | |
palatte.create_line(linelist[i - 1][0] * 50, | |
linelist[i - 1][1] * 50, | |
linelist[i][0] * 50, | |
linelist[i][1] * 50, | |
fill = linelist[i - 1][2], width = 8, | |
capstyle = ROUND) | |
palatte.pack() | |
def frostcake(): | |
"""Converts list to arduino-readable format, parses by nozzle type and | |
color, and sends commands as needed""" | |
index = 0 | |
origin = (0, 0) | |
color = self.pointslist[0][0][2] | |
nozzle = self.pointslist[0][0][3] | |
# X- and Y-distances rather than the single absolute distance, for | |
# which we already have a function | |
def distances(point1, point2): | |
distx = point2[0] - point1[0] | |
disty = point2[1] - point1[1] | |
dist = (distx, disty) | |
return dist | |
def sendcommands(index, origin, color, nozzle): | |
myCurrentCommands = [] | |
clist = self.pointslist[index] | |
temppoint = distances(origin, clist[0]) | |
if index == 0: | |
try: | |
myCurrentCommands.append(('/,')) | |
myCurrentCommands.append((0, temppoint[0], temppoint[1])) | |
except: | |
print "Failed to send startup key" | |
else: | |
myCurrentCommands.append((0, temppoint[0], temppoint[1])) | |
# Run through the current list, converting points to distances | |
# and adding a binary indicator of whether the machine is | |
# frosting, or just moving to begin a new shape | |
for i in range(1, len(self.pointslist[index])): | |
point = distances(clist[i - 1], clist[i]) | |
myCurrentCommands.append((1, point[0], point[1])) | |
index = index + 1 | |
if index == len(self.pointslist): | |
lastpoint = distances((self.pointslist[-1][-1][0], | |
self.pointslist[-1][-1][1]), | |
(0, 0)) | |
myCurrentCommands.append((0, lastpoint[0], lastpoint[1])) | |
sendtoserial(myCurrentCommands) | |
return | |
sendtoserial(myCurrentCommands) | |
origin = clist[-1] | |
clist = self.pointslist[index] | |
# If the color and nozzle type of the next list are the same, | |
# continue frosting as before. If they have changed, create | |
# a pop-up message asking the user to change them and waiting | |
# until the user confirms before continuing to send commands | |
if clist[0][2] == color and clist[0][3] == nozzle: | |
sendcommands(index, origin, color, nozzle) | |
else: | |
def dothis(): | |
change.destroy() | |
sendcommands(index, origin, clist[0][2], | |
clist[0][3]) | |
change = Tk() | |
change.title('Team Cake') | |
change.resizable(0, 0) | |
canvas = Canvas(change, width = 300, height = 80) | |
canvas.create_text((150, 50), text = "The color or nozzle \ | |
needs changing. \n Please make the frosting color %s. \n Please make the \ | |
nozzle shape a %s. \n When you have done so, press the button." \ | |
%(clist[0][2], clist[0][3]), | |
width = 300) | |
quitbutton = Button(change, text = 'The color and nozzle \ | |
are correct. Keep frosting', command = dothis) | |
canvas.grid(padx = 5) | |
quitbutton.grid(pady = 10, padx = 5) | |
def sendtoserial(CCommands): | |
"""Sends commands to the arduino""" | |
try: | |
print "Trying..." | |
arduino = serial.Serial('COM3', 9600) | |
print arduino.readline() | |
except: | |
print "Failed to connect." | |
for i in CCommands: | |
if len(i) == 2: | |
try: | |
print "Trying %s" %i | |
arduino.write(i) | |
print arduino.readline() | |
except: | |
print "Failed to send the startup command" | |
else: | |
print "Trying yn...." | |
print "sending %i" % (i[0]) | |
try: | |
arduino.write("%i" % (i[0])) | |
print arduino.readline() | |
except: | |
print "Failed to send yn!" | |
print "Trying x...." | |
print "sending %i" % (i[1]*1000) | |
try: | |
arduino.write("%i" % (i[1] * 1000)) | |
print arduino.readline() | |
except: | |
print "Failed to send x!" | |
print "Trying y...." | |
print "sending %i" % (i[2]*1000) | |
try: | |
arduino.write("%i" % (i[2] * 1000)) | |
print arduino.readline() | |
except: | |
print "Failed to send y!" | |
sendcommands(index, origin, color, nozzle) | |
print "Frosting complete!" | |
# Create all color buttons for the paint GUI | |
red = Button(bg = 'red', command = Callable(setcolor, 'red')) | |
blue = Button(bg = 'blue', command = Callable(setcolor, 'blue')) | |
green = Button(bg = 'green', command = Callable(setcolor, 'green')) | |
white = Button(bg = 'white', command = Callable(setcolor, 'white')) | |
yellow = Button(bg = 'yellow', command = Callable(setcolor, 'yellow')) | |
clearbutton = Button(text = 'Start over.', command = clear) | |
# Create all nozzle buttons for the paint GUI | |
circlepic = PhotoImage(file = "circlenozzle.gif", master = root) | |
circbutton = Button(root, image = circlepic, | |
command = Callable(setnozzle, 'circle')) | |
circbutton.image = circlepic | |
diamondpic = PhotoImage(file = "twodiamond.gif", master = root) | |
diamondbutton = Button(root, image = diamondpic, | |
command = Callable(setnozzle, 'diamond')) | |
diamondbutton.image = diamondpic | |
toothedmoonpic = PhotoImage(file = "toothedmoon.gif", master = root) | |
toothedbutton = Button(root, image = toothedmoonpic, | |
command = Callable(setnozzle, 'toothedmoon'),) | |
toothedbutton.image = toothedmoonpic | |
moonpic = PhotoImage(file = "moonnozzle.gif", master = root) | |
moonbutton = Button(root, image = moonpic, | |
command = Callable(setnozzle, 'moon')) | |
moonbutton.image = moonpic | |
bigstarpic = PhotoImage(file = "bigstar.gif", master = root) | |
bigstarbutton = Button(root, image = bigstarpic, | |
command = Callable(setnozzle, 'bigstar')) | |
bigstarbutton.image = bigstarpic | |
smallstarpic = PhotoImage(file = "smallstar.gif", master = root) | |
smallstarbutton = Button(root, image = smallstarpic, | |
command = Callable(setnozzle, 'smallstar')) | |
smallstarbutton.image = smallstarpic | |
# Create preview and frost buttons for the paint GUI | |
previewcanvas = Button(text = 'Preview this cake.', | |
command = preview) | |
frostcanvas = Button(text = 'Frost this cake.', command = frostcake) | |
# Fit all of the paint GUI buttons into the appropriate grid | |
red.grid(column = 0, row = 0, sticky = N + W + E + S) | |
blue.grid(column = 1, row = 0, sticky = N + W + E + S) | |
white.grid(column = 0, row = 1, sticky = N + W + E + S) | |
green.grid(column = 1, row = 1, sticky = N + W + E + S) | |
yellow.grid(column = 0, row = 2, sticky = N + W + E + S) | |
clearbutton.grid(column = 1, row = 2, sticky = N + W + E + S) | |
circbutton.grid(column = 0, row = 3, sticky = N + W + E + S) | |
diamondbutton.grid(column = 1, row = 3, sticky = N + W + E + S) | |
toothedbutton.grid(column = 0, row = 4, sticky = N + W + E + S) | |
moonbutton.grid(column = 1, row = 4, sticky = N + W + E + S) | |
bigstarbutton.grid(column = 0, row = 5, sticky = N + W + E + S, | |
rowspan = 2) | |
smallstarbutton.grid(column = 1, row = 5, sticky = N + W + E + S, | |
rowspan = 2) | |
canvas.grid(column = 2, row = 0, columnspan = 2, rowspan = 6) | |
previewcanvas.grid(column = 2, row = 6, sticky = W + E + S) | |
frostcanvas.grid(column = 3, row = 6, sticky = W + E + S) | |
root.grid_columnconfigure(0, minsize = 50, weight = 1) | |
root.grid_columnconfigure(1, minsize = 50, weight = 1) | |
###----BELOW IS THE SIZE-SPECIFICATION GUI, WHICH LAUNCHES THE PAINT GUI------ | |
# Create a warning pop-up message is the user-inputed size exceeds the maximum | |
# cake size our machine can handle | |
def sizewarning(): | |
warn = Tk() | |
warn.title('Team Cake') | |
warn.resizable(0,0) | |
canvas = Canvas(warn, width = 300, height = 80) | |
canvas.create_text((150, 50), text = "We're sorry. Cakes have a \ | |
maximum length of 13 inches, a maximum width of 11 inches, and a maximum \ | |
diameter of 11 inches. Please enter valid cake dimensions.", width = 280) | |
quitbutton = Button(warn, text = "Let me try again.", | |
command = warn.destroy) | |
canvas.grid() | |
quitbutton.grid(pady = 10) | |
def close_window(): | |
canvas.destroy() | |
# Get correct size, regardless of whether the cake is rectangular or circular | |
def buttonstuff(): | |
size = [] | |
if v1.get() == 0: | |
if lengthentry.get() == '' or widthentry.get() == '': | |
sizewarning() | |
else: | |
length = int(lengthentry.get()) | |
width = int(widthentry.get()) | |
size = [length, width] | |
elif v1.get() == 1: | |
if diameterentry.get() == '': | |
sizewarning() | |
else: | |
diameter = int(diameterentry.get()) | |
size = [diameter] | |
# We like inches. So we're converting to them if the user was silly enough | |
# to use centimeters. | |
if v2.get() == 1: | |
for i in range(len(size)): | |
size[i] = size[i] / 2.54 | |
#Check to make sure that they entered suitable dimensions. If not, raise | |
#a warning, otherwise, close the selection menu and make a paint GUI | |
if len(size) == 2: | |
if size[0] > 13 or size[1] > 11: | |
sizewarning() | |
else: | |
g.destroy() | |
cake = Mycake(size, cakecolor) | |
cake.PaintGUI() | |
elif len(size) == 1: | |
if size[0] > 11: | |
sizewarning() | |
else: | |
g.destroy() | |
cake = Mycake(size, cakecolor) | |
cake.PaintGUI() | |
# Set the default color for the cake color drop-down menu and do sketchy things | |
# (yes, this involves a global variable) to make the menu work | |
cakecolor = '#F6F6B1' | |
menutitle = 'Select a color.' | |
def setcake(color, title): | |
global cakecolor | |
cakecolor = color | |
menumb.configure(text = title) | |
# Make the size selection GUI | |
g = Tk() | |
g.title('Team Cake') | |
g.resizable(0,0) | |
# Make all labels to go in GUI | |
label1 = Label(text = 'Please enter the dimensions of your cake.') | |
label2 = Label(text = 'Please choose your units.') | |
label3 = Label(text = 'Length:') | |
label4 = Label(text = 'Width:') | |
label5 = Label(text = 'Diameter:') | |
label6 = Label(text = 'What color is your cake?') | |
blank = Label(text = ' ') | |
# Make all text-entry fields to go in GUI | |
lengthentry = Entry(g, width = 5) | |
widthentry = Entry(g, width = 5) | |
diameterentry = Entry(g, width = 15) | |
# Make all radiobuttons to go in GUI | |
v1 = IntVar() | |
v2 = IntVar() | |
rectchoice = Radiobutton(text = 'Rectangle', variable = v1, value = 0) | |
circchoice = Radiobutton(text = 'Circle', variable = v1, value = 1) | |
incheschoice = Radiobutton(text = 'in', variable = v2, value = 0) | |
centchoice = Radiobutton(text = 'cm', variable = v2, value = 1) | |
# Make the "I'm ready" button to go in GUI | |
button = Button(text = "I'm ready to design an amazing cake.", | |
command = buttonstuff) | |
# Make the cake color selection drop-down menu to go in GUI | |
menumb = Menubutton(text = menutitle, relief = 'raised', bd = 2) | |
menumb.menu = Menu(menumb, tearoff = 0) | |
menumb["menu"] = menumb.menu | |
menumb.menu.add_command(label = 'Yellow', | |
command = Callable(setcake, '#F6F6B1', 'Yellow')) | |
menumb.menu.add_command(label = 'Chocolate', | |
command = Callable(setcake, 'tan4', 'Chocolate')) | |
menumb.menu.add_command(label = 'White', | |
command = Callable(setcake, 'white', 'White')) | |
# Lay out all items in GUI in a grid | |
label1.grid(row = 0, column = 0, columnspan = 6, pady = 8) | |
rectchoice.grid(row = 1, column = 0, sticky = W, padx = 2, pady = 4) | |
blank.grid(row = 1, column = 1) | |
label3.grid(row = 1, column = 2) | |
lengthentry.grid(row = 1, column = 3, padx = 4) | |
label4.grid(row = 1, column = 4) | |
widthentry.grid(row = 1, column = 5, padx = 4) | |
circchoice.grid(row = 2, column = 0, sticky = W, padx = 2, pady = 2) | |
label5.grid(row = 2, column = 2) | |
diameterentry.grid(row = 2, column = 3, columnspan = 3, sticky = E + W, | |
padx = 4) | |
label2.grid(row = 3, column = 0, columnspan = 3, pady = 6) | |
incheschoice.grid(row = 3, column = 3) | |
centchoice.grid(row = 3, column = 4) | |
label6.grid(row = 4, column = 0, columnspan = 3) | |
menumb.grid(row = 4, column = 3, columnspan = 3, padx = 6, pady = 6, | |
sticky = N + E + S + W) | |
button.grid(row = 5, column = 0, columnspan = 6, pady = 6, | |
padx = 6, sticky = W + E) | |
# Start doing things! | |
g.mainloop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment