#!/usr/bin/env python # -*- coding: iso-8859-1 -*- # This is Overslack, a slackline simulator. # For more information, see http://www.lysator.liu.se/~jc/overslack.html # Copyright (C) 2003-2005 Jörgen Cederlöf # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA # 02111-1307, USA. version="0.5" import sys import os from cmath import * import time import Tkinter #import tkFileDialog configfile = os.path.expanduser('~/.overslackrc') def warn(s): print >>sys.stderr, s class Range: def __init__(self, min, max, bounded=0): if max < min: raise ValueError("Error in range: min [%s] > max [%s]" % (`min`, `max`)) self.min = min + 0. self.max = max + 0. self.len = self.max - self.min self.bounded = bounded def __repr__(self): return "Range(%.3f, %.3f)" % (self.min, self.max) def __contains__(self, right): return self.min <= right <= self.max def __rdiv__(self, value): if self.bounded: return max(0., min(1., (value - self.min) / self.len)) else: return (value - self.min) / self.len def __mul__(self, procent): return self.min + self.len*procent def __rmul__(self, procent): return self.min + self.len*procent def __rmod__(self, value): return min(self.max, max(self.min, value)) def __eq__(self, othr): if not isinstance(othr, Range): return NotImplemented return (self.min, self.max, self.bounded) == \ (othr.min, othr.max, othr.bounded) def __ne__(self, othr): if not isinstance(othr, Range): return NotImplemented return not self.__eq__(othr) def middle(self): return (self.min + self.max) / 2.0 class cRange: def __init__(self, min, max, bounded=0): for _max, _min in (max.real,min.real), (max.imag,min.imag): if _max < _min: raise ValueError("Error in range: min [%s] > max [%s]" % (`min`, `max`)) self.min = min self.max = max self.len = self.max - self.min self.bounded = bounded def __repr__(self): return "cRange((%.3f + %.3fj), (%.3f + %.3fj))" \ % (self.min.real, self.min.imag, self.max.real, self.max.imag) def __contains__(self, right): return self.min.real <= right.real <= self.max.real and \ self.min.imag <= right.imag <= self.max.imag def __rdiv__(self, value): r = (value.real - self.min.real) / self.len.real i = (value.imag - self.min.imag) / self.len.imag if self.bounded: return max(0., min(1., r)) +\ max(0., min(1., i)) *1j else: return r+i*1j def __rmul__(self, procent): return (self.min.real + self.len.real * procent.real) + \ (self.min.imag + self.len.imag * procent.imag) * 1j def __mul__(self, procent): return self.__rmul__(procent) def __rmod__(self, value): return min(self.max.real, max(self.min.real, value.real)) + \ min(self.max.real, max(self.min.real, value.real)) * 1j def __eq__(self, othr): if not isinstance(othr, cRange): return NotImplemented return (self.min, self.max, self.bounded) == \ (othr.min, othr.max, othr.bounded) def __ne__(self, othr): if not isinstance(othr, cRange): return NotImplemented return not self.__eq__(othr) class Config: def __init__(self, tree, configvalues): self.__tree = [] self.__sectionnames = [] self.__values = {} self.__types = {} self.__infos = {} self.__valuestrings = {} for section in tree: self.__sectionnames.append((section[0], section[1])) self.__tree.append([]) for entry in section[2]: self.__tree[-1].append(entry[0]) self.__types[entry[0]] = entry[1] self.__infos[entry[0]] = entry[2:] # (unit, description) self[entry[0]] = configvalues[entry[0]] def __set(self, name, value): def compl(str): imag,real = str.split(",") imag,real = float(imag), float(real) return complex(real, imag) type = self.__types[name] # c=complex f=float r=range b=bounded range k=key if type=="f": val = float(value) elif type=="c": val = compl(value) elif type[0]=="r" or type[0]=="b": if type[0]=="b": bounded=1 else: bounded=0 min,max = value.split(":") if type[1] == "f": val = Range(float(min), float(max)) else: val = cRange(compl(min), compl(max)) elif type=="k": val = value.split() else: raise ValueError("Internal error: Unknown config type") self.__values[name] = val self.__valuestrings[name] = value def getnrsections(self): return len(self.__tree) def getsectionname(self, sectionnr): """Return (name, visibility)""" return self.__sectionnames[sectionnr] def getnrentries(self, sectionnr): return len(self.__tree[sectionnr]) # Return (name, value, unit, desc) def getentry(self, sectionnr, entrynr): name = self.__tree[sectionnr][entrynr] return (name, self.__valuestrings[name], self.__infos[name][0], self.__infos[name][1]) def __setitem__(self, name, value): self.__set(name, value) # Allow errors introduced when typing a value def checkloose(self, name, value): type = self.__types[name] allowedchars="0123456789.-" if "c" in type: allowedchars += "," if "b" in type or "r" in type: allowedchars += ":" for char in value: if char not in allowedchars: return 0 return 1 def __getitem__(self, name): return self.__values[name] def save(self, f): print >>f, "# This is an overslack configuration file." print >>f, "# See http://www.lysator.liu.se/~jc/overslack.html" print >>f, "# Everything after # on a line is ignored." print >>f, "# Empty lines are ignored." print >>f, "# If you remove a line, that value is unchanged" print >>f, "# when the configuration is loaded." print >>f for (n, items) in enumerate(self.__tree): name, visibility = self.__sectionnames[n] if visibility == "visible": print >>f, '# Section "%s"' % name else: print >>f, '# Section "%s" (Not shown in GUI) ' % name for itemnr in range(len(items)): (name, value, unit, desc) = self.getentry(n, itemnr) begin = "%s %s " % (name, value) pad = " " * (40-len(begin)) end = "# [%s] %s" % (unit, desc) print >>f, begin+pad+end print >>f print >>f, "# End of configuration." def configload(f): """Generate keys and values for configuration from a config file.""" for line in f: line = line.split('#', 1)[0].strip() if line: key, value = line.split(' ', 1) yield key, value configtree = [ # c=complex f=float r=range b=bounded range k=key ("Screen", "hidden", # j-direction is left. 1-direction is up. [ ("initialsize", "c", "pixels", "Initial size of screen"), ]), ("Update rates", "visible", [ ("framerate", "f", "Hz", "Screen update rate"), ("physrate", "f", "Hz", "Physical state update rate"), ("inforate", "f", "Hz", "On-screen info update rate") ]), ("World", "visible", [ ("ph_g", "f", "m/s^2", "Gravity"), ("timescale", "f", "s/s", "Run X times faster"), ("groundsky", "rf", "m", "Ground and sky level") ]), ("Slacker", "visible", # 0.1 would be more realistic... But on the other hand, a wider # foot emulates reflexes to move feet into position. [ ("sl_feetwidth", "f", "m", "Width of feet"), ("sl_leglength", "rf", "m", "Leg length range"), ("sl_armrot", "rf", "rad", "Arms rotation range"), # Mass moment of inertia = integrate(m*|r|²) # where r is vector from rotational center # A uniform rod with length 1.8m and mass 80kg has I=21.6kg*m^2. ("sl_arms_inertia_range", "bf", "kg*m^2", "Moment of inertia of arms"), ("sl_body_inertia_range", "bf", "kg*m^2", "Moment of inertia of body excluding arms"), ("sl_mass", "f", "kg", "Total slacker mass") ]), ("Line", "visible", # Realistic value: 9.82m/s^2*75kg/0.{5,4}m [ ("dl_e_range", "bf", "N/m", "Spring constant range") ]), ("Keys", "hidden", [ ("keys_quit", "k", "keys", "Keys to quit"), ("keys_help", "k", "keys", "Keys to view help"), ("keys_toggle_info", "k", "keys", "Keys to toggle OSD info"), ("keys_configure", "k", "keys", "Keys to show configuration window"), ("keys_toggle_pause", "k", "keys", "Keys to toggle pause"), ("keys_toggle_help_vectors", "k", "keys", "Keys to toggle help vectors"), ("keys_reset", "k", "keys", "Keys to reset state") ]) ] presets = {"Tight line": {"groundsky": "-2:5", "dl_e_range": "1473:1841"}, "Loose line": {"groundsky": "-3:5", "dl_e_range": "273:341"}, "Midline": {"groundsky": "-12:3", "dl_e_range": "1000:1100"}, "Highline": {"groundsky": "-1000:50", "dl_e_range": "173:241"}, "Realistic": {"sl_arms_inertia_range": "4:10", "sl_armrot": "-1.5:1.5"}, "Easy": {"sl_arms_inertia_range": "10:14", "sl_armrot": "-6:6"}, "Slow": {"timescale": "0.2"}, "Fast": {"timescale": "1"}, "Default config": {"initialsize": "800,500", "framerate": "60", "physrate": "10000", "inforate": "5", "ph_g": "9.82", "timescale": "0.2", "groundsky": "-2:5", # 0.1 would be more realistic... But on # the other hand, a wider foot emulates # reflexes to move feet into position. "sl_feetwidth": "0.5", "sl_leglength": "0.5:1.3", "sl_armrot": "-6:6", # Mass moment of inertia = # integrate(m*|r|²) where r is vector # from rotational center A uniform rod # with length 1.8m and mass 80kg has # I=21.6kg*m^2. "sl_arms_inertia_range": "10:14", "sl_body_inertia_range": "6:10", "sl_mass": "75", # Realistic value: # 9.82m/s^2*75kg/0.{5,4}m "dl_e_range": "1473:1841", "keys_quit": "q", "keys_help": "h", "keys_toggle_info": "i", "keys_configure": "c", "keys_toggle_pause": "b1 p", "keys_toggle_help_vectors": "b2 v", "keys_reset": "b3 r", } } presetsorder = ["Tight line", "Loose line", "Midline", "Highline", "----", "Realistic", "Easy", "----", "Fast", "Slow", "----", "Default config"] def uvector(angle): return exp(1j*angle) def crosslength(v0, v1): return v0.real*v1.imag - v0.imag*v1.real def dot(v0, v1): return v0.real*v1.real + v0.imag*v1.imag # Check if a square is around origo def aroundorigo(p0, p1, p2, p3): return crosslength(p0, p1-p0) <= 0 and \ crosslength(p1, p2-p1) <= 0 and \ crosslength(p2, p3-p2) <= 0 and \ crosslength(p3, p0-p3) <= 0 class Physics: def __init__(self, config): self.__config = config self.__physicstime = 0. self.__lastrealtime = time.time() self.g = config["ph_g"] self.__timescale = config["timescale"] self.timescale = self.__timescale self.__paused = 0 def __updatephysicstime(self): last, now = self.__lastrealtime, time.time() self.__lastrealtime = now self.__physicstime += (now-last) * self.timescale # Just calculate current time without changing anything, to avoid # many errors piling up. def physicstime(self): last, now = self.__lastrealtime, time.time() return self.__physicstime + (now-last) * self.timescale def readconfig(self): self.__updatephysicstime() self.g = self.__config["ph_g"] self.__timescale = self.__config["timescale"] if not self.__paused: self.timescale = self.__timescale def pause(self): self.__updatephysicstime() if not self.__paused: self.timescale = 0 self.__paused = 1 def unpause(self): self.__updatephysicstime() if self.__paused: self.timescale = self.__timescale self.__paused = 0 def toggle_pause(self): if not self.__paused: self.pause() else: self.unpause() class Body: def __init__(self, config): self.__config = config self.pos = 0 + 0j self.posp = 0 + 0j self.pospp = 0 + 0j self.rot = 0. self.rotp = 0. self.rotpp = 0. self.mass = 1. self.inertia = 60. def readconfig(self): self.g = self.__config["ph_g"] def applyforce(self, force, where, dt): self.posp += dt * force / self.mass self.pospp += force / self.mass self.rotp += dt * crosslength(where, force) / self.inertia self.rotpp += crosslength(where, force) / self.inertia def speedatpoint(self, where): """Calculate the speed of a point of the body.""" return self.posp + where * 1j * self.rotp def bounce(self, where, bouncefactor=1.999): # Calculate speed at point spd = self.speedatpoint(where) # Speed vector from middle: mspd = where * dot(spd, where) / dot(where, where) # Stop center speed: centerimpulse = -mspd*self.mass # Speed vector in rotational direction: rspd = spd - mspd # Stop rotational speed: if dot(where, where) != 0: rotimpulse = -rspd / (dot(where, where)/self.inertia + 1/self.mass) else: rotimpulse = 0j # Apply impulse self.applyforce((centerimpulse+rotimpulse)*bouncefactor, where, 1) def Et(self): "Return transitional kinetic energy" return self.mass * dot(self.posp, self.posp) / 2. def Er(self): "Return rotational kinetic energy" return self.inertia * self.rotp**2 / 2. def Ep(self, h): "Return potential energy for height h." return self.mass * h * self.g def P(self): "Return momentum" return self.mass * self.posp def Pa(self): "Return angular momentum" return self.inertia * self.rotp def stealkineticenergy(self, E): """Steal E first from transitional kinetic energy, and if that is not enough also from rotational kinetic energy. Leave 5% of the energy to keep the body from stopping completely.""" Et = self.Et() Er = self.Er() if Et*0.95 >= E: self.posp *= sqrt(max((Et-E)/Et, 0.05)).real elif (Et+Er)*0.95 >= E: self.posp *= sqrt(0.05) E -= 0.95*Et self.rotp *= sqrt(max((Er-E)/Er, 0.05)).real else: # This will only steal a part of E. self.posp *= sqrt(0.05).real self.rotp *= sqrt(0.05).real def evolve(self, dt): self.pos += dt*self.posp # Update position due to speed self.rot += dt*self.rotp # Update rotation # We begin on a new dt. Reset acceleration counters. def resetacc(self): self.pospp = 0. self.rotpp = 0. class Slacker(Body): def __init__(self, config): self.__config = config Body.__init__(self, config) self.readconfig() self.__posture = 1 + 0j # [m + rad*1j] self.reset() def readconfig(self): config = self.__config Body.readconfig(self) self.__body_inertia_range = config["sl_body_inertia_range"] self.__arms_inertia_range = config["sl_arms_inertia_range"] self.mass = config["sl_mass"] self.__feetwidth = config["sl_feetwidth"] self.__posture_range = cRange(complex(config["sl_leglength"].min, config["sl_armrot"] .min), complex(config["sl_leglength"].max, config["sl_armrot"] .max)) def reset(self): springconstant = self.__config["dl_e_range"].middle() # [N/m] balance = self.mass * self.__config["ph_g"] / springconstant # [m] self.rot = 0. self.rotp = 0. self.pos = self.__posture.real - balance + 0j self.posp = 0 + 0j self.linepos = - self.__posture.real*uvector(self.rot) self.__update_inertia(0) self.__new_feet_state = self.rot, self.pos, self.__posture.real self.__old_feet_state = self.rot, self.pos, self.__posture.real def applyforce(self, force, where, dt, line=None): # Line cannot pull if line and dot(force, where) > 0: self.linepos = None return Body.applyforce(self, force, where, dt) def evolve(self, dt): Body.evolve(self, dt) self.__old_feet_state = self.__new_feet_state self.__new_feet_state = self.rot+pi, self.pos, self.__posture.real if self.linepos is not None: # This only works when standing on foot. self.linepos = - self.__posture.real*uvector(self.rot) return # Check if feet lands on line. The rest of the body is handled # in drawbody(). This special case for the feet are intended # to make the simulation more realistic. In reality, you have # reflexes that will make your feet move to the line if they # are a little bit on either side. You also have the # possibility to move the line to the middle of your feet # while standing on it. Therefore, if feet moves past the line # close enough, move the line to the middle of the feet. oldhalffoot = uvector(self.__old_feet_state[0]) * 1j * self.__feetwidth/2 newhalffoot = uvector(self.__new_feet_state[0]) * 1j * self.__feetwidth/2 oldfootpos = uvector(self.__old_feet_state[0]) \ * self.__old_feet_state[2] + self.__old_feet_state[1] newfootpos = uvector(self.__new_feet_state[0]) \ * self.__new_feet_state[2] + self.__new_feet_state[1] p0 = oldfootpos - oldhalffoot p1 = oldfootpos + oldhalffoot p2 = newfootpos + newhalffoot p3 = newfootpos - newhalffoot if aroundorigo(p0, p1, p2, p3): self.linepos = newfootpos - self.pos def bodyoptions(self, px_per_meter): def s(v, limit=0): # Scale return max(limit, v * px_per_meter / 67) return [("l_sl_legs", {"fill": "#999", "width": s(5, 2), "arrow": "first", "arrowshape": (s(10),s(0),s(10))}), ("l_sl_arms", {"fill": "#bbb", "width": s(3, 2), "arrow": "both", "arrowshape": (s(10),s(4),s(4))}), ("c_sl_head", {"fill": "#99f"})] def drawbody(self, leftwall, groundlevel, rightwall): lines = {} bodyrotv = uvector(self.rot) armsrotv = uvector(self.rot + self.__posture.imag) lines["l_sl_legs"] = (-self.__posture.real * bodyrotv, 0.5 * bodyrotv) centerarms = 0.4 * bodyrotv arm = 0.8j * armsrotv lines["l_sl_arms"] = (centerarms-arm, centerarms+arm) lines["c_sl_head"] = (0.7 * bodyrotv, 0.2) bounced = 0 for line in lines: coords = lines[line] if line[0] == "l": # Line if coords[0].real < coords[1].real: low = coords[0] else: low = coords[1] if coords[0].imag < coords[1].imag: right, left = coords else: left, right = coords elif line[0] == "c": # Circle low = coords[0] - coords[1] right = coords[0] - coords[1]*1j left = coords[0] + coords[1]*1j if (self.pos + low).real < groundlevel: # We want to correct the error caused by non-infinite # update rate by moving the slacker up, but by doing # so we give him some energy which we must steal from # something else. sink = (self.pos + low - groundlevel).real self.stealkineticenergy(self.Ep(-sink)) self.pos -= sink if self.speedatpoint(low).real <= 0: bounced = 1 self.bounce(low) if (self.pos + left ).imag > leftwall: self.pos -= (self.pos + left - leftwall *1j).imag*1j if self.speedatpoint(low).imag >= 0: self.bounce(left) if (self.pos + right).imag < rightwall: self.pos -= (self.pos + right - rightwall*1j).imag*1j if self.speedatpoint(low).imag <= 0: self.bounce(right) return bounced, lines def feetpos(self): return uvector(self.rot) * -self.__posture.real # Update inertia to the new leg length, and rotate if needed. def __update_inertia(self, changerot=1): old_total_inertia = self.inertia # Linear is simplest. p = self.__posture.real/self.__posture_range.max.real self.__arms_inertia = p * self.__arms_inertia_range self.__body_inertia = p * self.__body_inertia_range self.inertia = self.__arms_inertia + self.__body_inertia if changerot: self.rotp *= old_total_inertia / self.inertia def control(self, xy): oldrot = self.__posture.imag xy = xy.conjugate() + 1j self.__posture = xy * self.__posture_range self.__update_inertia() # Total angular moment must be conserved. self.rot -= (self.__posture.imag - oldrot) * self.__arms_inertia / self.inertia class Line: def __init__(self, config): self.__config = config self.pos = 0 + 0j self.__speed = 0. self.__lastpos = self.pos self.readconfig() def readconfig(self): self.__e_range = self.__config["dl_e_range"] # Simulate something very similar to an ideal spring, but with # friction def force(self, sdt): if sdt != 0: # Speed _from_ center [m/s] self.__speed = (abs(self.pos)-abs(self.__lastpos)) / sdt self.__lastpos = self.pos return -self.pos * (self.__e_range * (atan(self.__speed*10).real/pi + 0.5)) # When calling this class, give arguments in meters. class Gui: def __init__(self, config, sl, dl): self.__config = config size = config["initialsize"] self.__initialsize = size self.__actualsize = size self.__px_range = cRange(0j, size) self.readconfig() self.__sl = sl self.__dl = dl self.__dirty = 1 self.__help_vectors_state = "normal" self.__events = [] self.__root = Tkinter.Tk() self.__root.title("Overslack") self.__root.geometry("%dx%d" % (size.imag, size.real)) self.__canvas = Tkinter.Canvas(self.__root, background="#012") # Menu self.__menu = Tkinter.Menu(self.__root) # File menu self.__menu_file = Tkinter.Menu(self.__menu, tearoff=0) self.__menu_file.add_command(label="Configure", command=self.configdialog) self.__menu_file.add_command(label="Quit", command=self.__cb_delete_window) self.__menu.add_cascade(label="File", menu=self.__menu_file) # Show menu self.__menu_show = Tkinter.Menu(self.__menu, tearoff=0) self.__menu_show.add_command(label="Help vectors", command=self.toggle_help_vectors) self.__menu_show.add_command(label="On-screen information", command=self.cb_osd) self.__menu.add_cascade(label="Show", menu=self.__menu_show) # Help menu self.__menu_help = Tkinter.Menu(self.__menu, tearoff=0) self.__menu_help.add_command(label="Help", command=self.__cb_help) self.__menu_help.add_command(label="About", command=self.__cb_about) self.__menu.add_cascade(label="Help", menu=self.__menu_help) self.__root.configure(menu=self.__menu) self.__canvas.pack(fill=Tkinter.BOTH, expand=1) self.__aboutvisible = 0 self.__helpvisible = 0 self.__configvisible = 0 self.__init_lines() self.__setsize(size) self.__root.protocol("WM_DELETE_WINDOW", self.__cb_delete_window) self.__root.bind("