changeset 1107:512381de45bd

effectclasss in subserver. multiline code suppport (except for evaulation). add some old effect funcs to the new evaluator Ignore-this: c9bb7359c851bc7c43fb48c50bf41d79
author Drew Perttula <drewp@bigasterisk.com>
date Tue, 10 Jun 2014 08:48:34 +0000
parents 95ed52dcc3ab
children 4b542d321c8f
files bin/effecteval light9/Effects.py light9/curvecalc/subterm.py light9/effecteval/effect.coffee light9/effecteval/effect.html light9/effecteval/effect.py light9/effecteval/effectloop.py light9/subserver/effects.coffee light9/subserver/effects.jade
diffstat 9 files changed, 180 insertions(+), 105 deletions(-) [+]
line wrap: on
line diff
--- a/bin/effecteval	Tue Jun 10 08:46:52 2014 +0000
+++ b/bin/effecteval	Tue Jun 10 08:48:34 2014 +0000
@@ -4,7 +4,7 @@
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks, returnValue
 import cyclone.web, cyclone.websocket, cyclone.httpclient
-import sys, optparse, logging, subprocess, json, time, traceback
+import sys, optparse, logging, subprocess, json, time, traceback, itertools
 from rdflib import URIRef, Literal
 
 sys.path.append(".")
@@ -39,24 +39,39 @@
         ctx = song
         graph = self.settings.graph
         effect = graph.sequentialUri(song + "/effect-")
-        curve = graph.sequentialUri(song + "/curve-")
+        quads = [
+            (song, L9['effect'], effect, ctx),
+            (effect, RDF.type, L9['Effect'], ctx),
+        ]
 
         with graph.currentState(
-                tripleFilter=(dropped, RDFS.label, None)) as g:
-            droppedSubLabel = g.label(dropped)
+                tripleFilter=(dropped, None, None)) as g:
+            droppedTypes = g.objects(dropped, RDF.type)
+            droppedLabel = g.label(dropped)
+            droppedCodes = g.objects(dropped, L9['code'])
+
+        if L9['EffectClass'] in droppedTypes:
+            quads.extend([
+                (effect, RDFS.label, droppedLabel, ctx),
+                (effect, RDF.type, dropped, ctx),
+                ] + [(effect, L9['code'], c, ctx) for c in droppedCodes])
+        elif L9['Curve'] in droppedTypes:
+            curve = graph.sequentialUri(song + "/curve-")
+            cr = CurveResource(graph, curve)
+            cr.newCurve(ctx, label=Literal('sub %s' % droppedLabel))
+            cr.saveCurve()
+            quads.extend([
+                (song, L9['curve'], curve, ctx),                
+                (effect, L9['code'],
+                 Literal('out = %s * %s' % (dropped.n3(), curve.n3())),
+                 ctx),
+                ])
+        else:
+            raise NotImplementedError(
+                "don't know how to add an effect from %r (types=%r)" %
+                (dropped, droppedTypes))
             
-        cr = CurveResource(graph, curve)
-        cr.newCurve(ctx, label=Literal('sub %s' % droppedSubLabel))
-        cr.saveCurve()
-        graph.patch(Patch(addQuads=[
-            (song, L9['curve'], curve, ctx),
-            (song, L9['effect'], effect, ctx),
-            (effect, RDF.type, L9['Effect'], ctx),
-            (effect, L9['code'],
-             Literal('out = %s * %s' % (dropped.n3(), curve.n3())),
-             ctx),
-            ]))
-        
+        graph.patch(Patch(addQuads=quads))
         
 class SongEffectsUpdates(cyclone.websocket.WebSocketHandler):
     def connectionMade(self, *args, **kwargs):
@@ -89,7 +104,8 @@
     def updateClient(self):
         # todo: if client has dropped, abort and don't get any more
         # graph updates
-        self.sendMessage({'code': self.graph.value(self.uri, L9['code'])})
+        self.sendMessage({'codeLines':
+                          list(self.graph.objects(self.uri, L9['code']))})
         
     def connectionLost(self, reason):
         log.info("websocket closed")
@@ -98,18 +114,41 @@
         log.info("got message %s" % message)
         # write a patch back to the graph
 
+def replaceObjects(graph, c, s, p, newObjs):
+    patch = graph.getObjectPatch(
+        context=c,
+        subject=s,
+        predicate=p,
+        newObject=newObjs[0])
+
+    moreAdds = []
+    for line in newObjs[1:]:
+        moreAdds.append((s, p, line, c))
+    fullPatch = Patch(delQuads=patch.delQuads,
+                      addQuads=patch.addQuads + moreAdds)
+    graph.patch(fullPatch)
+
+        
 class Code(PrettyErrorHandler, cyclone.web.RequestHandler):
     def put(self):
         effect = URIRef(self.get_argument('uri'))
-        code = Literal(self.get_argument('code'))
+        codeLines = []
+        for i in itertools.count(0):
+            k = 'codeLines[%s][text]' % i
+            v = self.get_argument(k, None)
+            if v is not None:
+                codeLines.append(Literal(v))
+            else:
+                break
+        if not codeLines:
+            log.info("no codelines recevied on PUT /code")
+            return
         with self.settings.graph.currentState(
                 tripleFilter=(None, L9['effect'], effect)) as g:
             song = g.subjects(L9['effect'], effect).next()
-        self.settings.graph.patchObject(
-            context=song,
-            subject=effect,
-            predicate=L9['code'],
-            newObject=code)
+            
+        replaceObjects(self.settings.graph, song, effect, L9['code'], codeLines)
+            
         # right here we could tell if the code has a python error and return it
         self.send_error(202)
         
--- a/light9/Effects.py	Tue Jun 10 08:46:52 2014 +0000
+++ b/light9/Effects.py	Tue Jun 10 08:48:34 2014 +0000
@@ -1,5 +1,6 @@
 from __future__ import division
-from random import Random
+import random as random_mod
+import math
 import logging, colorsys
 import light9.Submaster as Submaster
 from chase import chase as chase_logic
@@ -9,12 +10,18 @@
 from light9.namespaces import L9
 log = logging.getLogger()
 
+registered = []
+def register(f):
+    registered.append(f)
+    return f
+
+@register
 def chase(t, ontime=0.5, offset=0.2, onval=1.0, 
           offval=0.0, names=None, combiner=max, random=False):
     """names is list of URIs. returns a submaster that chases through
     the inputs"""
     if random:
-        r = Random(random)
+        r = random_mod.Random(random)
         names = names[:]
         r.shuffle(names)
 
@@ -31,6 +38,7 @@
 
     return Submaster.Submaster(name="chase" ,levels=lev)
 
+@register
 def hsv(h, s, v, light='all', centerScale=.5):
     r,g,b = colorsys.hsv_to_rgb(h % 1.0, s, v)
     lev = {}
@@ -41,7 +49,8 @@
     if light in ['center', 'all']:
         lev[88], lev[89], lev[90] = r*centerScale,g*centerScale,b*centerScale
     return Submaster.Submaster(name='hsv', levels=lev)
-    
+
+@register
 def stack(t, names=None, fade=0):
     """names is list of URIs. returns a submaster that stacks the the inputs
 
@@ -65,6 +74,10 @@
     
     return Submaster.Submaster(name="stack", levels=lev)
 
+@register
+def smoove(x):
+    return -2 * (x ** 3) + 3 * (x ** 2)
+    
 def configExprGlobals():
     graph = showconfig.getGraph()
     ret = {}
@@ -75,7 +88,35 @@
         ret[shortName] = list(graph.items(chans))
         print "%r is a chase" % shortName
 
-    ret['chase'] = chase
-    ret['stack'] = stack
-    ret['hsv'] = hsv
+    for f in registered:
+        ret[f.__name__] = f
+
+    ret['nsin'] = lambda x: (math.sin(x * (2 * math.pi)) + 1) / 2
+    ret['ncos'] = lambda x: (math.cos(x * (2 * math.pi)) + 1) / 2
+
+    _smooth_random_items = [random_mod.random() for x in range(100)]
+
+    # suffix '2' to keep backcompat with the versions that magically knew time
+    def smooth_random2(t, speed=1):
+        """1 = new stuff each second, <1 is slower, fade-ier"""
+        x = (t * speed) % len(_smooth_random_items)
+        x1 = int(x)
+        x2 = (int(x) + 1) % len(_smooth_random_items)
+        y1 = _smooth_random_items[x1]
+        y2 = _smooth_random_items[x2]
+        return y1 + (y2 - y1) * ((x - x1))
+
+    def notch_random2(t, speed=1):
+        """1 = new stuff each second, <1 is slower, notch-ier"""
+        x = (t * speed) % len(_smooth_random_items)
+        x1 = int(x)
+        y1 = _smooth_random_items[x1]
+        return y1
+
+    ret['noise2'] = smooth_random2
+    ret['notch2'] = notch_random2
+
+
+
+    
     return ret
--- a/light9/curvecalc/subterm.py	Tue Jun 10 08:46:52 2014 +0000
+++ b/light9/curvecalc/subterm.py	Tue Jun 10 08:48:34 2014 +0000
@@ -13,7 +13,6 @@
     e.g. chases"""
     def __init__(self):
         self.effectGlobals = light9.Effects.configExprGlobals()
-        self._smooth_random_items = [random.random() for x in range(100)]
     
     def exprGlobals(self, startDict, t):
         """globals dict for use by expressions"""
@@ -23,50 +22,27 @@
         # add in functions from Effects
         glo.update(self.effectGlobals)
 
-        glo['nsin'] = lambda x: (math.sin(x * (2 * math.pi)) + 1) / 2
-        glo['ncos'] = lambda x: (math.cos(x * (2 * math.pi)) + 1) / 2
+        def chan(name):
+            return Submaster.Submaster(
+                name=name,
+                levels={get_dmx_channel(name) : 1.0})
+        glo['chan'] = chan
         glo['within'] = lambda a, b: a < t < b
         glo['bef'] = lambda x: t < x
 
-
-        def smoove(x):
-            return -2 * (x ** 3) + 3 * (x ** 2)
-        glo['smoove'] = smoove
-
         def aft(t, x, smooth=0):
             left = x - smooth / 2
             right = x + smooth / 2
             if left < t < right:
-                return smoove((t - left) / (right - left))
+                return light9.Effects.smoove((t - left) / (right - left))
             return t > x
         glo['aft'] = lambda x, smooth=0: aft(t, x, smooth)
 
-        def chan(name):
-            return Submaster.Submaster(
-                name=name,
-                levels={get_dmx_channel(name) : 1.0})
-        glo['chan'] = chan
-
-        def smooth_random(speed=1):
-            """1 = new stuff each second, <1 is slower, fade-ier"""
-            x = (t * speed) % len(self._smooth_random_items)
-            x1 = int(x)
-            x2 = (int(x) + 1) % len(self._smooth_random_items)
-            y1 = self._smooth_random_items[x1]
-            y2 = self._smooth_random_items[x2]
-            return y1 + (y2 - y1) * ((x - x1))
-
-        def notch_random(speed=1):
-            """1 = new stuff each second, <1 is slower, notch-ier"""
-            x = (t * speed) % len(self._smooth_random_items)
-            x1 = int(x)
-            y1 = self._smooth_random_items[x1]
-            return y1
-            
-        glo['noise'] = smooth_random
-        glo['notch'] = notch_random
-
+        glo['smooth_random'] = lambda speed=1: glo['smooth_random2'](t, speed)
+        glo['notch_random'] = lambda speed=1: glo['notch_random2'](t, speed)
         
+        glo['noise'] = glo['smooth_random']
+        glo['notch'] = glo['notch_random']
 
         return glo
 
--- a/light9/effecteval/effect.coffee	Tue Jun 10 08:46:52 2014 +0000
+++ b/light9/effecteval/effect.coffee	Tue Jun 10 08:48:34 2014 +0000
@@ -1,19 +1,20 @@
 qs = new QueryString()
 model =
-  uri: ko.observable(qs.value('uri'))
-  code: ko.observable()
+  toSave: 
+    uri: ko.observable(qs.value('uri'))
+    codeLines: ko.observableArray([])
   
 socket = reconnectingWebSocket "ws://localhost:8070/effectUpdates" + window.location.search, (msg) ->
   console.log('effectData ' + JSON.stringify(msg))
-  # there's a shorter unpack thing
-    
-  model.code(msg.code)
-  
-writeBack = ko.computed ->
+  model.toSave.codeLines(msg.codeLines.map((x) -> {text: ko.observable(x)})) if msg.codeLines?
+
+model.saveCode = ->
   $.ajax
     type: 'PUT'
     url: 'code'
-    data: {uri: model.uri(), code: model.code()}
+    data: ko.toJS(model.toSave)
+
+writeBack = ko.computed(model.saveCode)
   
 ko.applyBindings(model)
   
\ No newline at end of file
--- a/light9/effecteval/effect.html	Tue Jun 10 08:46:52 2014 +0000
+++ b/light9/effecteval/effect.html	Tue Jun 10 08:48:34 2014 +0000
@@ -7,9 +7,14 @@
 
   </head>
   <body>
-    <a href="./">Effects</a> / <a class="effect" data-bind="attr: {href: uri}, text: uri"></a>
+    <a href="./">Effects</a> / <a class="effect" data-bind="attr: {href: toSave.uri}, text: toSave.uri"></a>
 
-    <div>code: <input type="text" size="160" data-bind="value: code"></input></div>
+    <div data-bind="foreach: toSave.codeLines">
+      <div>
+        code:
+        <input type="text" size="160" data-bind="value: text"></input>
+      </div>
+    </div>
     
     <script src="static/jquery-2.1.1.min.js"></script>
     <script src="static/knockout-3.1.0.js"></script>
--- a/light9/effecteval/effect.py	Tue Jun 10 08:46:52 2014 +0000
+++ b/light9/effecteval/effect.py	Tue Jun 10 08:48:34 2014 +0000
@@ -2,7 +2,7 @@
 from rdflib import URIRef
 from light9.namespaces import L9, RDF
 from light9.curvecalc.curve import CurveResource
-from light9 import Submaster
+from light9 import Submaster, Effects
 log = logging.getLogger('effect')
 
 def uriFromCode(s):
@@ -23,6 +23,7 @@
         self.graph, self.code = graph, code
 
         self.outName, self.expr, self.resources = self._asPython()
+        self.pyResources = self._resourcesAsPython(self.resources)
 
     def _asPython(self):
         """
@@ -55,32 +56,14 @@
         # this result could vary with graph changes (rare)
         return self.graph.contains((uri, RDF.type, L9['Curve']))
         
-class EffectNode(object):
-    def __init__(self, graph, uri):
-        self.graph, self.uri = graph, uri
-        # this is not expiring at the right time, when an effect goes away
-        self.graph.addHandler(self.prepare)
-
-    def prepare(self):
-        log.info("prepare effect %s", self.uri)
-        # maybe there can be multiple lines of code as multiple
-        # objects here, and we sort them by dependencies
-        codeStr = self.graph.value(self.uri, L9['code'])
-        if codeStr is None:
-            raise ValueError("effect %s has no code" % self.uri)
-
-        self.code = CodeLine(self.graph, codeStr)
-
-        self.resourceMap = self.resourcesAsPython()
-        
-    def resourcesAsPython(self):
+    def _resourcesAsPython(self, resources):
         """
         mapping of the local names for uris in the code to high-level
         objects (Submaster, Curve)
         """
         out = {}
         subs = Submaster.get_global_submasters(self.graph)
-        for localVar, uri in self.code.resources.items():
+        for localVar, uri in resources.items():
             for rdfClass in self.graph.objects(uri, RDF.type):
                 if rdfClass == L9['Curve']:
                     cr = CurveResource(self.graph, uri)
@@ -92,21 +75,39 @@
                     out[localVar] = uri
 
         return out
-        
+
+class EffectNode(object):
+    def __init__(self, graph, uri):
+        self.graph, self.uri = graph, uri
+        # this is not expiring at the right time, when an effect goes away
+        self.graph.addHandler(self.prepare)
+
+    def prepare(self):
+        log.info("prepare effect %s", self.uri)
+        # maybe there can be multiple lines of code as multiple
+        # objects here, and we sort them by dependencies
+        codeStrs = list(self.graph.objects(self.uri, L9['code']))
+        if not codeStrs:
+            raise ValueError("effect %s has no code" % self.uri)
+
+        self.codes = [CodeLine(self.graph, s) for s in codeStrs]
+
+        self.otherFuncs = Effects.configExprGlobals()
+                
     def eval(self, songTime):
         ns = {'t': songTime}
+        ns.update(self.otherFuncs)
 
         ns.update(dict(
             curve=lambda c, t: c.eval(t),
             ))
         # loop over lines in order, merging in outputs 
         # merge in named outputs from previous lines
-        
-        ns.update(self.resourceMap)
-        return eval(self.code.expr, ns)
-
 
-class GraphAwareFunction(object):
-    def __init__(self, graph):
-        self.graph = graph
+        for c in self.codes:
+            codeNs = ns.copy()
+            codeNs.update(c.pyResources)
+            if c.outName == 'out':
+                out = eval(c.expr, codeNs)
+        return out
 
--- a/light9/effecteval/effectloop.py	Tue Jun 10 08:46:52 2014 +0000
+++ b/light9/effecteval/effectloop.py	Tue Jun 10 08:48:34 2014 +0000
@@ -79,7 +79,8 @@
                         if now > self.lastErrorLog + 5:
                             log.error("effect %s: %s" % (e.uri, exc))
                             log.error("  expr: %s", e.code.expr)
-                            log.error("  resources: %r", e.resourcesAsPython())
+                            log.error("  resources: %r",
+                                      getattr(e, 'resourceMap', '?'))
                             self.lastErrorLog = now
                 out = Submaster.sub_maxes(*outSubs)
 
--- a/light9/subserver/effects.coffee	Tue Jun 10 08:46:52 2014 +0000
+++ b/light9/subserver/effects.coffee	Tue Jun 10 08:48:34 2014 +0000
@@ -1,5 +1,7 @@
 class Model
   constructor: ->
+    @classes = ko.observable([])
+    
     @chases = ko.observable([])
     @moreExprs = [
       {label: "rainbow", expr: "hsv(t*1,1,1)"},
@@ -22,12 +24,11 @@
     ]
   
 
-
 model = new Model()
 
-
 reconnectingWebSocket "ws://localhost:8052/effectsUpdates", (msg) ->
   model.chases(msg.chases) if msg.chases?
+  model.classes(msg.classes) if msg.classes?
 
 # this sort of works to stop clicks in <input> from following the
 # submaster hyperlink, but it may make certain clicks act wrong
--- a/light9/subserver/effects.jade	Tue Jun 10 08:46:52 2014 +0000
+++ b/light9/subserver/effects.jade	Tue Jun 10 08:48:34 2014 +0000
@@ -19,6 +19,16 @@
         ul(data-bind="foreach: $parent.subtermExprs($data)")
           li: a.resource(data-bind="attr: {href: $root.subtermLink($parent.label, $data)}, text: $data")
 
+
+    div(data-bind="foreach: classes")
+      div.resource.effectClass
+        h2
+          | Effect class
+          | 
+          a(data-bind="attr: {href: uri}, text: label")
+        div
+          code(data-bind="text: code")
+        
     #status
       
     script(src="static/jquery-2.1.1.min.js")