comparison light9/curve.py @ 362:fc87327e29c4

CC now attaches to hardware sliders and knobs. tres cool. KC gets a --sliders option to enable the sliders
author Drew Perttula <drewp@bigasterisk.com>
date Fri, 15 Jun 2007 06:04:55 +0000
parents ff914126f3ea
children 058f2f508504
comparison
equal deleted inserted replaced
361:ff914126f3ea 362:fc87327e29c4
1 from __future__ import division 1 from __future__ import division
2 import sys,math,glob,random,os 2 import sys,math,glob,random,os, time
3 from bisect import bisect_left,bisect,bisect_right 3 from bisect import bisect_left,bisect,bisect_right
4 import Tkinter as tk 4 import Tkinter as tk
5 try: 5 try:
6 from dispatch import dispatcher 6 from dispatch import dispatcher
7 except ImportError: 7 except ImportError:
10 import run_local 10 import run_local
11 from light9 import Submaster, dmxclient, networking, cursors 11 from light9 import Submaster, dmxclient, networking, cursors
12 from light9.TLUtility import make_attributes_from_args 12 from light9.TLUtility import make_attributes_from_args
13 from light9.dmxchanedit import gradient 13 from light9.dmxchanedit import gradient
14 from light9.zoomcontrol import RegionZoom 14 from light9.zoomcontrol import RegionZoom
15 from bcf2000 import BCF2000
15 16
16 class Curve: 17 class Curve:
17 """curve does not know its name. see Curveset""" 18 """curve does not know its name. see Curveset"""
18 points = None # x-sorted list of (x,y) 19 points = None # x-sorted list of (x,y)
19 def __init__(self): 20 def __init__(self):
59 bisect(self.points, (x2,None)) + beyond) 60 bisect(self.points, (x2,None)) + beyond)
60 return range(leftidx, rightidx) 61 return range(leftidx, rightidx)
61 62
62 def points_between(self, x1, x2): 63 def points_between(self, x1, x2):
63 return [self.points[i] for i in self.indices_between(x1,x2)] 64 return [self.points[i] for i in self.indices_between(x1,x2)]
65
66 def point_before(self, x):
67 """(x,y) of the point left of x, or None"""
68 leftidx = self.index_before(x)
69 if leftidx is None:
70 return None
71 return self.points[leftidx]
72
73 def index_before(self, x):
74 leftidx = bisect(self.points, (x,None)) - 1
75 if leftidx < 0:
76 return None
77 return leftidx
78
64 79
65 def vlen(v): 80 def vlen(v):
66 return math.sqrt(v[0]*v[0] + v[1]*v[1]) 81 return math.sqrt(v[0]*v[0] + v[1]*v[1])
67 82
68 def angle_between(base, p0, p1): 83 def angle_between(base, p0, p1):
125 140
126 self.curveview.update_curve() 141 self.curveview.update_curve()
127 142
128 143
129 class Curveview(tk.Canvas): 144 class Curveview(tk.Canvas):
130 def __init__(self,master,curve,**kw): 145 def __init__(self, master, curve, knobEnabled=False, **kw):
146 """knobEnabled=True highlights the previous key and ties it to a
147 hardware knob"""
131 self.curve=curve 148 self.curve=curve
149 self.knobEnabled = knobEnabled
132 self._time = 0 150 self._time = 0
133 self.last_mouse_world = None 151 self.last_mouse_world = None
134 tk.Canvas.__init__(self,master,width=10,height=10, 152 tk.Canvas.__init__(self,master,width=10,height=10,
135 relief='sunken',bd=1, 153 relief='sunken',bd=1,
136 closeenough=5,takefocus=1, **kw) 154 closeenough=5,takefocus=1, **kw)
137 self.selected_points=[] # idx of points being dragged 155 self.selected_points=[] # idx of points being dragged
138 self.update_curve() 156 self.update_curve()
139 # self.bind("<Enter>",self.focus) 157 # self.bind("<Enter>",self.focus)
140 dispatcher.connect(self.input_time,"input time") 158 dispatcher.connect(self.input_time,"input time")
141 dispatcher.connect(self.update_curve,"zoom changed") 159 dispatcher.connect(self.update_curve,"zoom changed")
142 dispatcher.connect(self.update_curve,"points changed",sender=self.curve) 160 dispatcher.connect(self.update_curve,"points changed",
161 sender=self.curve)
143 dispatcher.connect(self.select_between,"select between") 162 dispatcher.connect(self.select_between,"select between")
163 if self.knobEnabled:
164 dispatcher.connect(self.knob_in, "knob in")
165 dispatcher.connect(self.slider_in, "slider in")
144 self.bind("<Configure>",self.update_curve) 166 self.bind("<Configure>",self.update_curve)
145 for x in range(1, 6): 167 for x in range(1, 6):
146 def add_kb_marker_point(evt, x=x): 168 def add_kb_marker_point(evt, x=x):
147 self.add_point((self.current_time(), (x - 1) / 4.0)) 169 self.add_point((self.current_time(), (x - 1) / 4.0))
148 170
189 self.bind("<ButtonRelease-1>", #"<Alt-KeyRelease>", 211 self.bind("<ButtonRelease-1>", #"<Alt-KeyRelease>",
190 self.select_release) 212 self.select_release)
191 213
192 self.bind("<ButtonPress-1>", self.check_deselect, add=True) 214 self.bind("<ButtonPress-1>", self.check_deselect, add=True)
193 215
216 def knob_in(self, curve, value):
217 """user turned a hardware knob, which edits the point to the
218 left of the current time"""
219 if curve != self.curve:
220 return
221 idx = self.curve.index_before(self.current_time())
222 if idx is not None:
223 pos = self.curve.points[idx]
224 self.curve.points[idx] = (pos[0], value)
225 self.update_curve()
226
227 def slider_in(self, curve, value):
228 """user pushed on a slider. make a new key"""
229 if curve != self.curve:
230 return
231
232 self.curve.insert_pt((self.current_time(), value))
233 self.update_curve()
234
194 def print_state(self, msg=""): 235 def print_state(self, msg=""):
195 if 0: 236 if 0:
196 print "%s: dragging_dots=%s selecting=%s" % ( 237 print "%s: dragging_dots=%s selecting=%s" % (
197 msg, self.dragging_dots, self.selecting) 238 msg, self.dragging_dots, self.selecting)
198 239
255 def world_from_screen(self,x,y): 296 def world_from_screen(self,x,y):
256 start,end = self.zoom 297 start,end = self.zoom
257 ht = self.winfo_height() 298 ht = self.winfo_height()
258 return x/self.winfo_width()*(end-start)+start, ((ht-5)-y)/(ht-10) 299 return x/self.winfo_width()*(end-start)+start, ((ht-5)-y)/(ht-10)
259 300
260 def input_time(self,val): 301 def input_time(self, val, forceUpdate=False):
302 # i tried various things to make this not update like crazy,
303 # but the timeline was always missing at startup, so i got
304 # scared that things were getting built in a funny order.
305 #if self._time == val:
306 # return
307
261 t=val 308 t=val
262 pts = self.screen_from_world((val,0))+self.screen_from_world((val,1)) 309 pts = self.screen_from_world((val,0))+self.screen_from_world((val,1))
263 self.delete('timecursor') 310 self.delete('timecursor')
264 self.create_line(*pts,**dict(width=2,fill='red',tags=('timecursor',))) 311 self.create_line(*pts,**dict(width=2,fill='red',tags=('timecursor',)))
312 self.have_time_line = True
265 self._time = t 313 self._time = t
314 if self.knobEnabled:
315 self.delete('knob')
316 prevKey = self.curve.point_before(t)
317 if prevKey is not None:
318 pos = self.screen_from_world(prevKey)
319 self.create_oval(pos[0] - 8, pos[1] - 8,
320 pos[0] + 8, pos[1] + 8,
321 outline='#800000',
322 tags=('knob',))
323 dispatcher.send("knob out", value=prevKey[1], curve=self.curve)
266 324
267 def update_curve(self,*args): 325 def update_curve(self,*args):
268 326
269 self.zoom = dispatcher.send("zoom area")[0][1] 327 self.zoom = dispatcher.send("zoom area")[0][1]
270 cp = self.curve.points 328 cp = self.curve.points
483 def dotrelease(self,ev): 541 def dotrelease(self,ev):
484 self.print_state("dotrelease") 542 self.print_state("dotrelease")
485 if not self.dragging_dots: 543 if not self.dragging_dots:
486 return 544 return
487 self.last_mouse_world = None 545 self.last_mouse_world = None
546 self.dragging_dots = False
547
548 class Sliders(BCF2000):
549 def __init__(self, cb, knobCallback):
550 BCF2000.__init__(self)
551 self.cb = cb
552 self.knobCallback = knobCallback
553 def valueIn(self, name, value):
554 if name.startswith("slider"):
555 self.cb(int(name[6:]), value / 127)
556 if name.startswith("knob"):
557 self.knobCallback(int(name[4:]), value / 127)
558
488 559
489 class Curveset: 560 class Curveset:
490 curves = None # curvename : curve 561 curves = None # curvename : curve
491 def __init__(self): 562 def __init__(self, sliders=False):
492 self.curves = {} 563 """sliders=True means support the hardware sliders"""
493 def load(self,basename): 564 self.curves = {} # name : Curve
565 self.curveName = {} # reverse
566 self.sliderCurve = {} # slider number (1 based) : curve name
567 self.sliderNum = {} # reverse
568 if sliders:
569 self.sliders = Sliders(self.hw_slider_in, self.hw_knob_in)
570 dispatcher.connect(self.curvesToSliders, "curves to sliders")
571 dispatcher.connect(self.knobOut, "knob out")
572 self.lastSliderTime = {} # num : time
573 self.sliderSuppressOutputUntil = {} # num : time
574 self.sliderIgnoreInputUntil = {}
575 else:
576 self.sliders = None
577
578 def load(self,basename, skipMusic=False):
494 """find all files that look like basename-curvename and add 579 """find all files that look like basename-curvename and add
495 curves with their contents""" 580 curves with their contents"""
496 for filename in glob.glob("%s-*"%basename): 581 for filename in glob.glob("%s-*"%basename):
497 curvename = filename[filename.rfind('-')+1:] 582 curvename = filename[filename.rfind('-')+1:]
583 if skipMusic and curvename in ['music', 'smooth_music']:
584 continue
498 c=Curve() 585 c=Curve()
499 c.load(filename) 586 c.load(filename)
500 curvename = curvename.replace('-','_') 587 curvename = curvename.replace('-','_')
501 self.add_curve(curvename,c) 588 self.add_curve(curvename,c)
502 def save(self,basename): 589 def save(self,basename):
503 """writes a file for each curve with a name 590 """writes a file for each curve with a name
504 like basename-curvename""" 591 like basename-curvename"""
505 for name,cur in self.curves.items(): 592 for name,cur in self.curves.items():
506 cur.save("%s-%s" % (basename,name)) 593 cur.save("%s-%s" % (basename,name))
594
507 def add_curve(self,name,curve): 595 def add_curve(self,name,curve):
508 self.curves[name] = curve 596 self.curves[name] = curve
509 dispatcher.send("add_curve",sender=self,name=name) 597 self.curveName[curve] = name
598
599 if self.sliders and name not in ['smooth_music', 'music']:
600 num = len(self.sliderCurve) + 1
601 self.sliderCurve[num] = name
602 self.sliderNum[name] = num
603 else:
604 num = None
605
606 dispatcher.send("add_curve", slider=num, knobEnabled=num is not None,
607 sender=self,name=name)
510 608
511 def globalsdict(self): 609 def globalsdict(self):
512 return self.curves.copy() 610 return self.curves.copy()
513 611
514 def get_time_range(self): 612 def get_time_range(self):
524 c = Curve() 622 c = Curve()
525 s,e = self.get_time_range() 623 s,e = self.get_time_range()
526 c.points.extend([(s,0), (e,0)]) 624 c.points.extend([(s,0), (e,0)])
527 self.add_curve(name,c) 625 self.add_curve(name,c)
528 626
627 def hw_slider_in(self, num, value):
628 try:
629 curve = self.curves[self.sliderCurve[num]]
630 except KeyError:
631 return
632
633 now = time.time()
634 if now < self.sliderIgnoreInputUntil.get(num):
635 return
636 # don't make points too fast. This is the minimum spacing
637 # between slider-generated points.
638 self.sliderIgnoreInputUntil[num] = now + .1
639
640 # don't push back on the slider for a little while, since the
641 # user might be trying to slowly move it. This should be
642 # bigger than the ignore time above.
643 self.sliderSuppressOutputUntil[num] = now + .2
644
645 dispatcher.send("slider in", curve=curve, value=value)
646
647 def hw_knob_in(self, num, value):
648 try:
649 curve = self.curves[self.sliderCurve[num]]
650 except KeyError:
651 return
652 dispatcher.send("knob in", curve=curve, value=value)
653
654 def curvesToSliders(self, t):
655 now = time.time()
656 for num, name in self.sliderCurve.items():
657 if now < self.sliderSuppressOutputUntil.get(num):
658 continue
659 # self.lastSliderTime[num] = now
660
661 value = self.curves[name].eval(t)
662 self.sliders.valueOut("slider%s" % num, value * 127)
663
664 def knobOut(self, curve, value):
665 try:
666 num = self.sliderNum[self.curveName[curve]]
667 except KeyError:
668 return
669 self.sliders.valueOut("knob%s" % num, value * 127)
529 670
530 class Curvesetview(tk.Frame): 671 class Curvesetview(tk.Frame):
531 curves = None # curvename : Curveview 672 curves = None # curvename : Curveview
532 def __init__(self,master,curveset,**kw): 673 def __init__(self, master, curveset, **kw):
533 self.curves = {} 674 self.curves = {}
534 self.curveset = curveset 675 self.curveset = curveset
535 tk.Frame.__init__(self,master,**kw) 676 tk.Frame.__init__(self,master,**kw)
536 677
537 f = tk.Frame(self,relief='raised',bd=1) 678 f = tk.Frame(self,relief='raised',bd=1)
548 entry.pack(side='left', fill='x',exp=1) 689 entry.pack(side='left', fill='x',exp=1)
549 entry.bind("<Key-Return>", new_curve) 690 entry.bind("<Key-Return>", new_curve)
550 691
551 dispatcher.connect(self.add_curve,"add_curve",sender=self.curveset) 692 dispatcher.connect(self.add_curve,"add_curve",sender=self.curveset)
552 693
553 def add_curve(self,name): 694 def add_curve(self,name, slider=None, knobEnabled=False):
554 f = tk.Frame(self,relief='raised',bd=1) 695 f = tk.Frame(self,relief='raised',bd=1)
555 f.pack(side='top',fill='both',exp=1) 696 f.pack(side='top',fill='both',exp=1)
556 697
557 698
558 leftside = tk.Frame(f) 699 leftside = tk.Frame(f)
559 leftside.pack(side='left') 700 leftside.pack(side='left')
560 701
561 collapsed = tk.IntVar() 702 collapsed = tk.IntVar()
562 txt = "curve %r" % name 703 txt = "curve '%s'" % name
563 if len(name) > 7: 704 if len(name) > 7:
564 txt = repr(name) 705 txt = name
565 tk.Label(leftside,text=txt,font="6x10", 706 tk.Label(leftside,text=txt,font="6x10",
566 width=15).pack(side='top') 707 width=15).pack(side='top')
567 708
709 sliderLabel = None
568 def cmd(): 710 def cmd():
569 if collapsed.get(): 711 if collapsed.get():
712 if sliderLabel:
713 sliderLabel.pack_forget()
570 f.pack(exp=0) 714 f.pack(exp=0)
571 else: 715 else:
716 if sliderLabel:
717 sliderLabel.pack(side='top')
572 f.pack(exp=1) 718 f.pack(exp=1)
573 tk.Checkbutton(leftside, text="collapsed", font="6x10", 719 tk.Checkbutton(leftside, text="collapsed", font="6x10",
574 variable=collapsed, command=cmd).pack(side='top') 720 variable=collapsed, command=cmd).pack(side='top')
575 721
576 cv = Curveview(f,self.curveset.curves[name]) 722 if slider is not None:
723 # slider should have a checkbutton, defaults to off for
724 # music tracks
725 sliderLabel = tk.Label(leftside, text="Slider %s" % slider,
726 fg='#800000', font='arial 12 bold')
727 sliderLabel.pack(side='top')
728
729 cv = Curveview(f, self.curveset.curves[name],
730 knobEnabled=knobEnabled)
577 cv.pack(side='left',fill='both',exp=1) 731 cv.pack(side='left',fill='both',exp=1)
578 self.curves[name] = cv 732 self.curves[name] = cv