# HG changeset patch # User drewp@bigasterisk.com # Date 1715565730 25200 # Node ID 4556eebe5d73dbea10c5e098be3c8009c73d3489 # Parent 623836db99afa79d4ea4a57baaaa88e472080f76 topdir reorgs; let pdm have its src/ dir; separate vite area from light9/ diff -r 623836db99af -r 4556eebe5d73 .pdm.toml --- a/.pdm.toml Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,2 +0,0 @@ -[python] -path = "/usr/bin/python3.10" diff -r 623836db99af -r 4556eebe5d73 bin/attic/bcf_puppet_demo --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/bcf_puppet_demo Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,17 @@ +#!/usr/bin/python +""" +tiny bcf2000 controller demo +""" +from bcf2000 import BCF2000 +from twisted.internet import reactor + + +class PuppetSliders(BCF2000): + + def valueIn(self, name, value): + if name == 'slider1': + self.valueOut('slider5', value) + + +b = PuppetSliders() +reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/attic/bumppad --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/bumppad Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,89 @@ +#!bin/python + +import sys, time, math +import tkinter as tk + +import run_local +import light9.dmxclient as dmxclient +from light9.TLUtility import make_attributes_from_args + +from light9.Submaster import Submaster, sub_maxes + + +class pad(tk.Frame): + levs = None # Submaster : level + + def __init__(self, master, root, mag): + make_attributes_from_args('master', 'mag') + tk.Frame.__init__(self, master) + self.levs = {} + for xy, key, subname in [ + ((1, 1), 'KP_Up', 'centered'), + ((1, 3), "KP_Down", 'third-c'), + ((0, 2), 'KP_Left', 'scoop-l'), + ((2, 2), 'KP_Right', 'scoop-r'), + ((1, 0), 'KP_Divide', 'cyc'), + ((0, 3), "KP_End", 'hottest'), + ((2, 3), 'KP_Next', 'deepblues'), + ((0, 4), 'KP_Insert', "zip_red"), + ((2, 4), 'KP_Delete', "zip_orange"), + ((3, 1), 'KP_Add', 'strobedim'), + ((3, 3), 'KP_Enter', 'zip_blue'), + ((1, 2), 'KP_Begin', 'scoop-c'), + ]: + + sub = Submaster(subname) + self.levs[sub] = 0 + + l = tk.Label(self, + font="arial 12 bold", + anchor='w', + height=2, + relief='groove', + bd=5, + text="%s\n%s" % (key.replace('KP_', ''), sub.name)) + l.grid(column=xy[0], row=xy[1], sticky='news') + + root.bind( + "" % key, lambda ev, sub=sub: self.bumpto(sub, 1)) + root.bind("" % key, + lambda ev, sub=sub: self.bumpto(sub, 0)) + + def bumpto(self, sub, lev): + now = time.time() + self.levs[sub] = lev * self.mag.get() + self.master.after_idle(self.output) + + def output(self): + dmx = sub_maxes(*[s * l + for s, l in list(self.levs.items())]).get_dmx_list() + dmxclient.outputlevels(dmx, clientid="bumppad") + + +root = tk.Tk() +root.tk_setPalette("maroon4") +root.wm_title("bumppad") +mag = tk.DoubleVar() + +tk.Label(root, + text="Keypad press/release activate sub; 1..5 set mag", + font="Helvetica -12 italic", + anchor='w').pack(side='bottom', fill='x') + +pad(root, root, mag).pack(side='left', fill='both', exp=1) + +magscl = tk.Scale(root, + orient='vertical', + from_=1, + to=0, + res=.01, + showval=1, + variable=mag, + label='mag', + relief='raised', + bd=1) +for i in range(1, 6): + root.bind("" % i, lambda ev, i=i: mag.set(math.sqrt((i) / 5))) +magscl.pack(side='left', fill='y') + +root.mainloop() diff -r 623836db99af -r 4556eebe5d73 bin/attic/captureDevice --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/captureDevice Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,199 @@ +#!bin/python +""" +Operate a motorized light and take pictures of it in every position. +""" +from rdflib import URIRef +from twisted.internet import reactor +from twisted.internet.defer import inlineCallbacks, Deferred + +import logging +import optparse +import os +import time +import treq +import cyclone.web, cyclone.websocket, cyclone.httpclient +from light9.metrics import metrics, metricsRoute +from run_local import log +from cycloneerr import PrettyErrorHandler + +from light9.namespaces import L9, RDF +from light9 import networking, showconfig +from rdfdb.syncedgraph import SyncedGraph +from light9.paint.capture import writeCaptureDescription +from light9.effect.settings import DeviceSettings +from light9.collector.collector_client import sendToCollector +from rdfdb.patch import Patch +from light9.zmqtransport import parseJsonMessage + + + +class Camera(object): + + def __init__(self, imageUrl): + self.imageUrl = imageUrl + + def takePic(self, uri, writePath): + log.info('takePic %s', uri) + return treq.get( + self.imageUrl).addCallbacks(lambda r: self._done(writePath, r), + log.error) + + @inlineCallbacks + def _done(self, writePath, response): + jpg = yield response.content() + try: + os.makedirs(os.path.dirname(writePath)) + except OSError: + pass + with open(writePath, 'w') as out: + out.write(jpg) + log.info('wrote %s', writePath) + + +def deferSleep(sec): + d = Deferred() + reactor.callLater(sec, d.callback, None) + return d + + +class Capture(object): + firstMoveTime = 3 + settleTime = .5 + + def __init__(self, graph, dev): + self.graph = graph + self.dev = dev + + def steps(a, b, n): + return [round(a + (b - a) * i / n, 5) for i in range(n)] + + startTime = time.time() + self.captureId = 'cap%s' % (int(startTime) - 1495170000) + self.toGather = [] + + #quantum + rxSteps = steps(.06, .952, 10) + rySteps = steps(0.1, .77, 5) + zoomSteps = steps(.12, .85, 3) + # aura + rxSteps = steps(0.15, .95, 10) + rySteps = steps(0, .9, 5) + zoomSteps = steps(.6, .9, 3) + + row = 0 + for ry in rySteps: + xSteps = rxSteps[:] + if row % 2: + xSteps.reverse() + row += 1 + for rx in xSteps: + for zoom in zoomSteps: + self.toGather.append( + DeviceSettings( + graph, + [ + (dev, L9['rx'], rx), + (dev, L9['ry'], ry), + (dev, L9['color'], '#ffffff'), + (dev, L9['zoom'], zoom), + #(dev, L9['focus'], 0.13), + ])) + + self.devTail = dev.rsplit('/')[-1] + self.session = URIRef('/'.join( + [showconfig.showUri(), 'capture', self.devTail, self.captureId])) + self.ctx = URIRef(self.session + '/index') + + self.graph.patch( + Patch(addQuads=[ + (self.session, RDF.type, L9['CaptureSession'], self.ctx), + ])) + + self.numPics = 0 + self.settingsCache = set() + self.step().addErrback(log.error) + + def off(self): + return sendToCollector(client='captureDevice', + session='main', + settings=DeviceSettings(self.graph, [])) + + @inlineCallbacks + def step(self): + if not self.toGather: + yield self.off() + yield deferSleep(1) + reactor.stop() + return + settings = self.toGather.pop() + + log.info('[%s left] move to %r', len(self.toGather), settings) + yield sendToCollector(client='captureDevice', + session='main', + settings=settings) + + yield deferSleep(self.firstMoveTime if self.numPics == + 0 else self.settleTime) + + picId = 'pic%s' % self.numPics + path = '/'.join(['capture', self.devTail, self.captureId, picId + ]) + '.jpg' + uri = URIRef(self.session + '/' + picId) + + yield camera.takePic(uri, os.path.join(showconfig.root(), path)) + self.numPics += 1 + + writeCaptureDescription(self.graph, self.ctx, self.session, uri, + self.dev, path, self.settingsCache, settings) + + reactor.callLater(0, self.step) + + +camera = Camera( + 'http://plus:8200/picamserve/pic?res=1080&resize=800&iso=800&redgain=1.6&bluegain=1.6&shutter=60000&x=0&w=1&y=0&h=.952' +) + + +class Attrs(PrettyErrorHandler, cyclone.web.RequestHandler): + + @metrics('set_attr').time() + def put(self): + client, clientSession, settings, sendTime = parseJsonMessage( + self.request.body) + self.set_status(202) + + +def launch(graph): + + cap = Capture(graph, dev=L9['device/aura5']) + reactor.listenTCP(networking.captureDevice.port, + cyclone.web.Application(handlers=[ + (r'/()', cyclone.web.StaticFileHandler, { + "path": "light9/web", + "default_filename": "captureDevice.html" + }), + metricsRoute(), + ]), + interface='::', + cap=cap) + log.info('serving http on %s', networking.captureDevice.port) + + +def main(): + parser = optparse.OptionParser() + parser.add_option("-v", + "--verbose", + action="store_true", + help="logging.DEBUG") + (options, args) = parser.parse_args() + log.setLevel(logging.DEBUG if options.verbose else logging.INFO) + + graph = SyncedGraph(networking.rdfdb.url, "captureDevice") + + graph.initiallySynced.addCallback(lambda _: launch(graph)).addErrback( + log.error) + reactor.run() + + +if __name__ == '__main__': + main() diff -r 623836db99af -r 4556eebe5d73 bin/attic/curvecalc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/curvecalc Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,574 @@ +#!bin/python +""" +now launches like this: +% bin/curvecalc http://light9.bigasterisk.com/show/dance2007/song1 + + + +todo: curveview should preserve more objects, for speed maybe + +""" + +import sys +import imp +sys.path.append('/usr/lib/python2.7/dist-packages') # For gtk +from twisted.internet import gtk3reactor +gtk3reactor.install() +from twisted.internet import reactor + +import time, textwrap, os, optparse, linecache, signal, traceback, json +import gi +from gi.repository import Gtk +from gi.repository import GObject +from gi.repository import Gdk + +from urllib.parse import parse_qsl +import louie as dispatcher +from rdflib import URIRef, Literal, RDF, RDFS +import logging + +from run_local import log +from light9 import showconfig, networking +from light9.curvecalc import curveview +from light9.curvecalc.curve import Curveset +from light9.curvecalc.curveedit import serveCurveEdit +from light9.curvecalc.musicaccess import Music +from light9.curvecalc.output import Output +from light9.curvecalc.subterm import Subterm +from light9.curvecalc.subtermview import add_one_subterm +from light9.editchoicegtk import EditChoice, Local +from light9.gtkpyconsole import togglePyConsole +from light9.namespaces import L9 +from light9.observable import Observable +from light9 import clientsession +from rdfdb.patch import Patch +from rdfdb.syncedgraph import SyncedGraph +from light9.wavelength import wavelength + + +class SubtermExists(ValueError): + pass + + +class Main(object): + + def __init__(self, graph, opts, session, curveset, music): + self.graph, self.opts, self.session = graph, opts, session + self.curveset, self.music = curveset, music + self.lastSeenInputTime = 0 + self.currentSubterms = [ + ] # Subterm objects that are synced to the graph + + self.setTheme() + wtree = self.wtree = Gtk.Builder() + wtree.add_from_file("light9/curvecalc/curvecalc.glade") + mainwin = wtree.get_object("MainWindow") + + mainwin.connect("destroy", self.onQuit) + wtree.connect_signals(self) + + mainwin.show_all() + + mainwin.connect("delete-event", lambda *args: reactor.crash()) + + def updateTitle(): + mainwin.set_title( + "curvecalc - %s" % + graph.label(graph.value(session, L9['currentSong']))) + + graph.addHandler(updateTitle) + + songChoice = Observable(None) # to be connected with the session song + + self.registerGraphToSongChoice(wtree, session, graph, songChoice) + self.registerSongChoiceToGraph(session, graph, songChoice) + self.registerCurrentPlayerSongToUi(wtree, graph, songChoice) + + ec = EditChoice(graph, songChoice, label="Editing song:") + wtree.get_object("currentSongEditChoice").add(ec) + ec.show() + + wtree.get_object("subterms").connect("add", self.onSubtermChildAdded) + + self.refreshCurveView() + + self.makeStatusLines(wtree.get_object("status")) + self.setupNewSubZone() + self.acceptDragsOnCurveViews() + + # may not work + wtree.get_object("paned1").set_position(600) + + def registerGraphToSongChoice(self, wtree, session, graph, songChoice): + + def setSong(): + current = graph.value(session, L9['currentSong']) + if not wtree.get_object("followPlayerSongChoice").get_active(): + songChoice(current) + dispatcher.send("song_has_changed") + + graph.addHandler(setSong) + + def registerSongChoiceToGraph(self, session, graph, songChoice): + self.muteSongChoiceUntil = 0 + + def songChoiceToGraph(newSong): + if newSong is Local: + raise NotImplementedError('what do i patch') + log.debug('songChoiceToGraph is going to set to %r', newSong) + + # I get bogus newSong values in here sometimes. This + # workaround may not even be helping. + now = time.time() + if now < self.muteSongChoiceUntil: + log.debug('muted') + return + self.muteSongChoiceUntil = now + 1 + + graph.patchObject(context=session, + subject=session, + predicate=L9['currentSong'], + newObject=newSong) + + songChoice.subscribe(songChoiceToGraph) + + def registerCurrentPlayerSongToUi(self, wtree, graph, songChoice): + """current_player_song 'song' param -> playerSong ui + and + current_player_song 'song' param -> songChoice, if you're in autofollow + """ + + def current_player_song(song): + # (this is run on every frame) + ps = wtree.get_object("playerSong") + if URIRef(ps.get_uri()) != song: + log.debug("update playerSong to %s", ps.get_uri()) + + def setLabel(): + ps.set_label(graph.label(song)) + + graph.addHandler(setLabel) + ps.set_uri(song) + if song != songChoice(): + if wtree.get_object("followPlayerSongChoice").get_active(): + log.debug('followPlayerSongChoice is on') + songChoice(song) + + dispatcher.connect(current_player_song, "current_player_song") + self.current_player_song = current_player_song + + def setupNewSubZone(self): + self.wtree.get_object("newSubZone").drag_dest_set( + flags=Gtk.DestDefaults.ALL, + targets=[Gtk.TargetEntry('text/uri-list', 0, 0)], + actions=Gdk.DragAction.COPY) + + def acceptDragsOnCurveViews(self): + w = self.wtree.get_object("curves") + w.drag_dest_set(flags=Gtk.DestDefaults.ALL, + targets=[Gtk.TargetEntry('text/uri-list', 0, 0)], + actions=Gdk.DragAction.COPY) + + def recv(widget, context, x, y, selection, targetType, time): + subUri = URIRef(selection.data.strip()) + print("into curves", subUri) + with self.graph.currentState(tripleFilter=(subUri, RDFS.label, + None)) as current: + subName = current.label(subUri) + + if '?' in subUri: + subName = self.handleSubtermDrop(subUri) + else: + try: + self.makeSubterm(subName, + withCurve=True, + sub=subUri, + expr="%s(t)" % subName) + except SubtermExists: + # we're not making sure the expression/etc are + # correct-- user mihgt need to fix things + pass + curveView = self.curvesetView.row(subName).curveView + t = self.lastSeenInputTime # curveView.current_time() # new curve hasn't heard the time yet. this has gotten too messy- everyone just needs to be able to reach the time source + print("time", t) + curveView.add_points([(t - .5, 0), (t, 1)]) + + w.connect("drag-data-received", recv) + + def onDragDataInNewSubZone(self, widget, context, x, y, selection, + targetType, time): + data = URIRef(selection.data.strip()) + if '?' in data: + self.handleSubtermDrop(data) + return + with self.graph.currentState(tripleFilter=(data, None, + None)) as current: + subName = current.label(data) + self.makeSubterm(newname=subName, + withCurve=True, + sub=data, + expr="%s(t)" % subName) + + def handleSubtermDrop(self, data): + params = parse_qsl(data.split('?')[1]) + flattened = dict(params) + self.makeSubterm(Literal(flattened['subtermName']), + expr=flattened['subtermExpr']) + + for cmd, name in params: + if cmd == 'curve': + self.curveset.new_curve(name) + return name + + def onNewCurve(self, *args): + dialog = self.wtree.get_object("newCurve") + entry = self.wtree.get_object("newCurveName") + # if you don't have songx, that should be the suggested name + entry.set_text("") + if dialog.run() == 1: + self.curveset.new_curve(entry.get_text()) + dialog.hide() + + def onRedrawCurves(self, *args): + dispatcher.send("all curves rebuild") + + def onSubtermsMap(self, *args): + # if this was called too soon, like in __init__, the gtktable + # would get its children but it wouldn't lay anything out that + # I can see, and I'm not sure why. Waiting for map event is + # just a wild guess. + self.graph.addHandler(self.set_subterms_from_graph) + + def onNewSubterm(self, *args): + self.makeSubterm(Literal(""), withCurve=False) + return + + # pretty sure i don't want this back, but not completely sure + # what the UX should be to get the new curve. + + dialog = self.wtree.get_object("newSubterm") + # the plan is to autocomplete this on existing subterm names + # (but let you make one up, too) + entry = self.wtree.get_object("newSubtermName").get_children()[0] + entry.set_text("") + entry.grab_focus() + if dialog.run() == 1: + newname = entry.get_text() + wc = self.wtree.get_object("newSubtermMakeCurve").get_active() + self.makeSubterm(newname, withCurve=wc) + dialog.hide() + + def currentSong(self): + + with self.graph.currentState(tripleFilter=(self.session, + L9['currentSong'], + None)) as current: + return current.value(self.session, L9['currentSong']) + + def songSubtermsContext(self): + return self.currentSong() + + def makeSubterm(self, newname, withCurve=False, expr=None, sub=None): + """ + raises SubtermExists if we had a subterm with a sub with the given + name. what about a no-sub term with the same label? who knows + """ + assert isinstance(newname, Literal), repr(newname) + if withCurve: + self.curveset.new_curve(newname) + if newname in self.all_subterm_labels(): + raise SubtermExists("have a subterm who sub is named %r" % newname) + with self.graph.currentState() as current: + song = self.currentSong() + for i in range(1000): + uri = song + "/subterm/%d" % i + if (uri, None, None) not in current: + break + else: + raise ValueError("can't pick a name for the new subterm") + + ctx = self.songSubtermsContext() + quads = [ + (uri, RDF.type, L9.Subterm, ctx), + (uri, RDFS.label, Literal(newname), ctx), + (self.currentSong(), L9['subterm'], uri, ctx), + ] + if sub is not None: + quads.append((uri, L9['sub'], sub, ctx)) + if expr is not None: + quads.append((uri, L9['expression'], Literal(expr), ctx)) + self.graph.patch(Patch(addQuads=quads)) + + return uri + + def all_subterm_labels(self): + """ + Literal labels of subs in subterms. doesn't currently include labels of the + subterm resources. I'm not sure what I'm going to do with + those. + """ + labels = [] + with self.graph.currentState() as current: + for st in current.objects( + current.value(self.session, L9['currentSong']), + L9['subterm']): + sub = current.value(st, L9['sub']) + if sub is not None: + labels.append(current.label(sub)) + return labels + + def set_subterms_from_graph(self): + """rebuild all the gtktable 'subterms' widgets and the + self.currentSubterms list""" + song = self.graph.value(self.session, L9['currentSong']) + + newList = [] + for st in set(self.graph.objects(song, L9['subterm'])): + log.debug("song %s has subterm %s", song, st) + term = Subterm(self.graph, st, self.songSubtermsContext(), + self.curveset) + newList.append(term) + self.currentSubterms[:] = newList + + master = self.wtree.get_object("subterms") + log.debug("removing subterm widgets") + [master.remove(c) for c in master.get_children()] + for term in self.currentSubterms: + add_one_subterm(term, self.curveset, master) + master.show_all() + log.debug("%s table children showing" % len(master.get_children())) + + def setTheme(self): + settings = Gtk.Settings.get_default() + settings.set_property("gtk-application-prefer-dark-theme", True) + + providers = [] + providers.append(Gtk.CssProvider()) + providers[-1].load_from_path("theme/Just-Dark/gtk-3.0/gtk.css") + providers.append(Gtk.CssProvider()) + providers[-1].load_from_data(''' + * { font-size: 92%; } + .button:link { font-size: 7px } + ''') + + screen = Gdk.Display.get_default_screen(Gdk.Display.get_default()) + for p in providers: + Gtk.StyleContext.add_provider_for_screen( + screen, p, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + + def onSubtermChildAdded(self, subtermsTable, *args): + # this would probably work, but isn't getting called + log.info("onSubtermChildAdded") + v = subtermsTable.get_parent().props.vadjustment + v.props.value = v.props.upper + + def onQuit(self, *args): + reactor.crash() + # there's a hang after this, maybe in sem_wait in two + # threads. I don't know whose they are. + # This fix affects profilers who want to write output at the end. + os.kill(os.getpid(), signal.SIGKILL) + + def onCollapseAll(self, *args): + self.curvesetView.collapseAll() + + def onCollapseNone(self, *args): + self.curvesetView.collapseNone() + + def onDelete(self, *args): + self.curvesetView.onDelete() + + def onPythonConsole(self, item): + ns = dict() + ns.update(globals()) + ns.update(self.__dict__) + togglePyConsole(self, item, ns) + + def onSeeCurrentTime(self, item): + dispatcher.send("see time") + + def onSeeTimeUntilEnd(self, item): + dispatcher.send("see time until end") + + def onZoomAll(self, item): + dispatcher.send("show all") + + def onPlayPause(self, item): + # since the X coord in a curveview affects the handling, one + # of them may be able to pick this up + results = dispatcher.send("onPlayPause") + times = [t for listener, t in results if t is not None] + self.music.playOrPause(t=times[0] if times else None) + + def onSave(self, *args): + # only doing curves still. I hope to eliminate all this. + log.info("saving curves") + self.curveset.save() + log.info("saved") + + def makeStatusLines(self, master): + """various labels that listen for dispatcher signals""" + for row, (signame, textfilter) in enumerate([ + ('input time', lambda t: "%.2fs" % t), + ('output levels', lambda levels: textwrap.fill( + "; ".join([ + "%s:%.2f" % (n, v) for n, v in list(levels.items())[:2] if v + > 0 + ]), 70)), + ('update period', lambda t: "%.1fms" % (t * 1000)), + ('update status', lambda x: str(x)), + ]): + key = Gtk.Label("%s:" % signame) + value = Gtk.Label("") + master.resize(row + 1, 2) + master.attach(key, 0, 1, row, row + 1) + master.attach(value, 1, 2, row, row + 1) + key.set_alignment(1, 0) + value.set_alignment(0, 0) + + dispatcher.connect(lambda val, value=value, tf=textfilter: value. + set_text(tf(val)), + signame, + weak=False) + dispatcher.connect(lambda val: setattr(self, 'lastSeenInputTime', val), + 'input time', + weak=False) + master.show_all() + + def refreshCurveView(self): + wtree = self.wtree + mtimes = [ + os.path.getmtime(f) for f in [ + 'light9/curvecalc/curveview.py', + 'light9/curvecalc/zoomcontrol.py', + ] + ] + + if (not hasattr(self, 'curvesetView') or + self.curvesetView._mtimes != mtimes): + print("reload curveview.py") + curvesVBox = wtree.get_object("curves") + zoomControlBox = wtree.get_object("zoomControlBox") + [curvesVBox.remove(c) for c in curvesVBox.get_children()] + [zoomControlBox.remove(c) for c in zoomControlBox.get_children()] + try: + linecache.clearcache() + imp.reload(curveview) + + # old ones are not getting deleted right + if hasattr(self, 'curvesetView'): + self.curvesetView.live = False + + # mem problem somewhere; need to hold a ref to this + self.curvesetView = curveview.Curvesetview( + self.graph, curvesVBox, zoomControlBox, self.curveset) + self.curvesetView._mtimes = mtimes + + # this is scheduled after some tk shuffling, to + # try to minimize the number of times we redraw + # the curve at startup. If tk is very slow, it's + # ok. You'll just get some wasted redraws. + self.curvesetView.goLive() + except Exception: + print("reload failed:") + traceback.print_exc() + if self.opts.reload: + reactor.callLater(1, self.refreshCurveView) + + +class MaxTime(object): + """ + looks up the time in seconds for the session's current song + """ + + def __init__(self, graph, session): + self.graph, self.session = graph, session + graph.addHandler(self.update) + + def update(self): + song = self.graph.value(self.session, L9['currentSong']) + if song is None: + self.maxtime = 0 + return + musicfilename = showconfig.songOnDisk(song) + self.maxtime = wavelength(musicfilename) + log.info("new max time %r", self.maxtime) + dispatcher.send("max time", maxtime=self.maxtime) + + def get(self): + return self.maxtime + + +def launch(args, graph, session, opts, startTime, music): + + try: + song = URIRef(args[0]) + graph.patchObject(context=session, + subject=session, + predicate=L9['currentSong'], + newObject=song) + except IndexError: + pass + + curveset = Curveset(graph=graph, session=session) + + log.debug("startup: output %s", time.time() - startTime) + + mt = MaxTime(graph, session) + dispatcher.connect(lambda: mt.get(), "get max time", weak=False) + + start = Main(graph, opts, session, curveset, music) + out = Output(graph, session, music, curveset, start.currentSubterms) + + dispatcher.send("show all") + + if opts.startup_only: + log.debug("quitting now because of --startup-only") + return + + def hoverTimeResponse(requestHandler): + results = dispatcher.send("onPlayPause") + times = [t for listener, t in results if t is not None] + if not times: + requestHandler.set_status(404) + requestHandler.write("not hovering over any time") + return + with graph.currentState(tripleFilter=(session, L9['currentSong'], + None)) as g: + song = g.value(session, L9['currentSong']) + json.dump({"song": song, "hoverTime": times[0]}, requestHandler) + + serveCurveEdit(networking.curveCalc.port, hoverTimeResponse, start.curveset) + + +def main(): + startTime = time.time() + parser = optparse.OptionParser() + parser.set_usage("%prog [opts] [songURI]") + parser.add_option("--debug", action="store_true", help="log at DEBUG") + parser.add_option("--reload", + action="store_true", + help="live reload of themes and code") + parser.add_option("--startup-only", + action='store_true', + help="quit after loading everything (for timing tests)") + parser.add_option("--profile", help='"hotshot" or "stat"') + clientsession.add_option(parser) + opts, args = parser.parse_args() + + log.setLevel(logging.DEBUG if opts.debug else logging.INFO) + + log.debug("startup: music %s", time.time() - startTime) + + session = clientsession.getUri('curvecalc', opts) + + music = Music() + graph = SyncedGraph(networking.rdfdb.url, "curvecalc") + + graph.initiallySynced.addCallback(lambda _: launch(args, graph, session, + opts, startTime, music)) + from light9 import prof + prof.run(reactor.run, profile=opts.profile) + + +main() diff -r 623836db99af -r 4556eebe5d73 bin/attic/curvecalc_all_subterms --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/curvecalc_all_subterms Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,6 @@ +#!/bin/zsh +echo broken: use a plain shell loop +exit 1 + + +for x (`ls $LIGHT9_SHOW/subterms`) { bin/curvecalc $x } diff -r 623836db99af -r 4556eebe5d73 bin/attic/dmx_color_test.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/dmx_color_test.py Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,20 @@ +#!bin/python +from run_local import log +import colorsys, time, logging +from light9 import dmxclient +from twisted.internet import reactor, task + +log.setLevel(logging.INFO) +firstDmxChannel = 10 + + +def step(): + hue = (time.time() * .2) % 1.0 + r, g, b = colorsys.hsv_to_rgb(hue, 1, 1) + chans = [r, g, b] + log.info(chans) + dmxclient.outputlevels([0] * (firstDmxChannel - 1) + chans, twisted=True) + + +task.LoopingCall(step).start(.05) +reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/attic/dmxserver --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/dmxserver Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,280 @@ +#!bin/python +""" +Replaced by bin/collector + + +this is the only process to talk to the dmx hardware. other clients +can connect to this server and present dmx output, and this server +will max ('pile-on') all the client requests. + +this server has a level display which is the final set of values that +goes to the hardware. + +clients shall connect to the xmlrpc server and send: + + their PID (or some other cookie) + + a length-n list of 0..1 levels which will represent the channel + values for the n first dmx channels. + +server is port 8030; xmlrpc method is called outputlevels(pid,levellist). + +todo: + save dmx on quit and restore on restart + if parport fails, run in dummy mode (and make an option for that too) +""" + +from twisted.internet import reactor +from twisted.web import xmlrpc, server +import sys, time, os +from optparse import OptionParser +import run_local +import txosc.dispatch, txosc. async +from light9.io import ParportDMX, UsbDMX + +from light9.updatefreq import Updatefreq +from light9 import networking + +from txzmq import ZmqEndpoint, ZmqFactory, ZmqPullConnection, ZmqRequestTimeoutError +import json + + +def startZmq(port, outputlevels): + zf = ZmqFactory() + e = ZmqEndpoint('bind', 'tcp://*:%s' % port) + s = ZmqPullConnection(zf, e) + + def onPull(message): + msg = json.loads(message[0]) + outputlevels(msg['clientid'], msg['levellist']) + + s.onPull = onPull + + +class ReceiverApplication(object): + """ + receive UDP OSC messages. address is /dmx/1 for dmx channel 1, + arguments are 0-1 floats for that channel and any number of + following channels. + """ + + def __init__(self, port, lightServer): + self.port = port + self.lightServer = lightServer + self.receiver = txosc.dispatch.Receiver() + self.receiver.addCallback("/dmx/*", self.pixel_handler) + self._server_port = reactor.listenUDP( + self.port, + txosc. async .DatagramServerProtocol(self.receiver), + interface='0.0.0.0') + print("Listening OSC on udp port %s" % (self.port)) + + def pixel_handler(self, message, address): + # this is already 1-based though I don't know why + startChannel = int(message.address.split('/')[2]) + levels = [a.value for a in message.arguments] + allLevels = [0] * (startChannel - 1) + levels + self.lightServer.xmlrpc_outputlevels("osc@%s" % startChannel, allLevels) + + +class XMLRPCServe(xmlrpc.XMLRPC): + + def __init__(self, options): + + xmlrpc.XMLRPC.__init__(self) + + self.clientlevels = {} # clientID : list of levels + self.lastseen = {} # clientID : time last seen + self.clientfreq = {} # clientID : updatefreq + + self.combinedlevels = [] # list of levels, after max'ing the clients + self.clientschanged = 1 # have clients sent anything since the last send? + self.options = options + self.lastupdate = 0 # time of last dmx send + self.laststatsprint = 0 # time + + # desired seconds between sendlevels() calls + self.calldelay = 1 / options.updates_per_sec + + print("starting parport connection") + self.parportdmx = UsbDMX(dimmers=90, port=options.dmx_device) + if os.environ.get('DMXDUMMY', 0): + self.parportdmx.godummy() + else: + self.parportdmx.golive() + + self.updatefreq = Updatefreq() # freq of actual dmx sends + self.num_unshown_updates = None + self.lastshownlevels = None + # start the loop + self.sendlevels() + + # the other loop + self.purgeclients() + + def purgeclients(self): + """forget about any clients who haven't sent levels in a while. + this runs in a loop""" + + purge_age = 10 # seconds + + reactor.callLater(1, self.purgeclients) + + now = time.time() + cids = list(self.lastseen.keys()) + for cid in cids: + lastseen = self.lastseen[cid] + if lastseen < now - purge_age: + print(("forgetting client %s (no activity for %s sec)" % + (cid, purge_age))) + try: + del self.clientlevels[cid] + except KeyError: + pass + del self.clientfreq[cid] + del self.lastseen[cid] + + def sendlevels(self): + """sends to dmx if levels have changed, or if we havent sent + in a while""" + + reactor.callLater(self.calldelay, self.sendlevels) + + if self.clientschanged: + # recalc levels + + self.calclevels() + + if (self.num_unshown_updates is None or # first time + self.options.fast_updates or # show always + ( + self.combinedlevels != self.lastshownlevels and # changed + self.num_unshown_updates > 5)): # not too frequent + self.num_unshown_updates = 0 + self.printlevels() + self.lastshownlevels = self.combinedlevels[:] + else: + self.num_unshown_updates += 1 + + if time.time() > self.laststatsprint + 2: + self.laststatsprint = time.time() + self.printstats() + + # used to be a fixed 1 in here, for the max delay between + # calls, instead of calldelay + if self.clientschanged or time.time( + ) > self.lastupdate + self.calldelay: + self.lastupdate = time.time() + self.sendlevels_dmx() + + self.clientschanged = 0 # clear the flag + + def calclevels(self): + """combine all the known client levels into self.combinedlevels""" + self.combinedlevels = [] + for chan in range(0, self.parportdmx.dimmers): + x = 0 + for clientlist in list(self.clientlevels.values()): + if len(clientlist) > chan: + # clamp client levels to 0..1 + cl = max(0, min(1, clientlist[chan])) + x = max(x, cl) + self.combinedlevels.append(x) + + def printlevels(self): + """write all the levels to stdout""" + print("Levels:", + "".join(["% 2d " % (x * 100) for x in self.combinedlevels])) + + def printstats(self): + """print the clock, freq, etc, with a \r at the end""" + + sys.stdout.write("dmxserver up at %s, [polls %s] " % ( + time.strftime("%H:%M:%S"), + str(self.updatefreq), + )) + for cid, freq in list(self.clientfreq.items()): + sys.stdout.write("[%s %s] " % (cid, str(freq))) + sys.stdout.write("\r") + sys.stdout.flush() + + def sendlevels_dmx(self): + """output self.combinedlevels to dmx, and keep the updates/sec stats""" + # they'll get divided by 100 + if self.parportdmx: + self.parportdmx.sendlevels([l * 100 for l in self.combinedlevels]) + self.updatefreq.update() + + def xmlrpc_echo(self, x): + return x + + def xmlrpc_outputlevels(self, cid, levellist): + """send a unique id for your client (name+pid maybe), then + the variable-length dmx levellist (scaled 0..1)""" + if levellist != self.clientlevels.get(cid, None): + self.clientlevels[cid] = levellist + self.clientschanged = 1 + self.trackClientFreq(cid) + return "ok" + + def xmlrpc_currentlevels(self, cid): + """get a list of levels we're currently sending out. All + channels beyond the list you get back, they're at zero.""" + # if this is still too slow, it might help to return a single + # pickled string + self.trackClientFreq(cid) + trunc = self.combinedlevels[:] + i = len(trunc) - 1 + if i < 0: + return [] + while trunc[i] == 0 and i >= 0: + i -= 1 + if i < 0: + return [] + trunc = trunc[:i + 1] + return trunc + + def trackClientFreq(self, cid): + if cid not in self.lastseen: + print("hello new client %s" % cid) + self.clientfreq[cid] = Updatefreq() + self.lastseen[cid] = time.time() + self.clientfreq[cid].update() + + +parser = OptionParser() +parser.add_option("-f", + "--fast-updates", + action='store_true', + help=('display all dmx output to stdout instead ' + 'of the usual reduced output')) +parser.add_option("-r", + "--updates-per-sec", + type='float', + default=20, + help=('dmx output frequency')) +parser.add_option("-d", + "--dmx-device", + default='/dev/dmx0', + help='dmx device name') +parser.add_option("-n", + "--dummy", + action="store_true", + help="dummy mode, same as DMXDUMMY=1 env variable") +(options, songfiles) = parser.parse_args() + +print(options) + +if options.dummy: + os.environ['DMXDUMMY'] = "1" + +port = networking.dmxServer.port +print("starting xmlrpc server on port %s" % port) +xmlrpcServe = XMLRPCServe(options) +reactor.listenTCP(port, server.Site(xmlrpcServe)) + +startZmq(networking.dmxServerZmq.port, xmlrpcServe.xmlrpc_outputlevels) + +oscApp = ReceiverApplication(9051, xmlrpcServe) + +reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/attic/effectListing --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/effectListing Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,4 @@ +#!/bin/sh +pnpm exec vite -c light9/effect/listing/web/vite.config.ts & +wait + diff -r 623836db99af -r 4556eebe5d73 bin/attic/effectSequencer --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/effectSequencer Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,5 @@ +#!/bin/zsh +pnpm exec vite -c light9/effect/sequencer/web/vite.config.ts & +pdm run uvicorn light9.effect.sequencer.service:app --host 0.0.0.0 --port 8213 --no-access-log +wait + diff -r 623836db99af -r 4556eebe5d73 bin/attic/effecteval --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/effecteval Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,283 @@ +#!bin/python + +from run_local import log +from twisted.internet import reactor +from twisted.internet.defer import inlineCallbacks, returnValue +import cyclone.web, cyclone.websocket, cyclone.httpclient +import sys, optparse, logging, json, itertools +from rdflib import URIRef, Literal + +from light9 import networking, showconfig +from light9.effecteval.effect import EffectNode +from light9.effect.edit import getMusicStatus, songNotePatch +from light9.effecteval.effectloop import makeEffectLoop +from light9.metrics import metrics, metricsRoute +from light9.namespaces import L9 +from rdfdb.patch import Patch +from rdfdb.syncedgraph import SyncedGraph + +from cycloneerr import PrettyErrorHandler +from light9.coffee import StaticCoffee + + + +class EffectEdit(PrettyErrorHandler, cyclone.web.RequestHandler): + + def get(self): + self.set_header('Content-Type', 'text/html') + self.write(open("light9/effecteval/effect.html").read()) + + def delete(self): + graph = self.settings.graph + uri = URIRef(self.get_argument('uri')) + with graph.currentState(tripleFilter=(None, L9['effect'], uri)) as g: + song = ctx = list(g.subjects(L9['effect'], uri))[0] + self.settings.graph.patch( + Patch(delQuads=[ + (song, L9['effect'], uri, ctx), + ])) + + +@inlineCallbacks +def currentSong(): + s = (yield getMusicStatus())['song'] + if s is None: + raise ValueError("no current song") + returnValue(URIRef(s)) + + +class SongEffects(PrettyErrorHandler, cyclone.web.RequestHandler): + + def wideOpenCors(self): + self.set_header('Access-Control-Allow-Origin', '*') + self.set_header('Access-Control-Allow-Methods', + 'GET, PUT, POST, DELETE, OPTIONS') + self.set_header('Access-Control-Max-Age', '1000') + self.set_header('Access-Control-Allow-Headers', + 'Content-Type, Authorization, X-Requested-With') + + def options(self): + self.wideOpenCors() + self.write('') + + @inlineCallbacks + def post(self): + self.wideOpenCors() + dropped = URIRef(self.get_argument('drop')) + + try: + song = URIRef(self.get_argument('uri')) + except Exception: # which? + song = yield currentSong() + + event = self.get_argument('event', default='default') + + note = self.get_argument('note', default=None) + if note is not None: + note = URIRef(note) + + log.info("adding to %s", song) + note, p = yield songNotePatch(self.settings.graph, + dropped, + song, + event, + ctx=song, + note=note) + + self.settings.graph.patch(p) + self.settings.graph.suggestPrefixes(song, {'song': URIRef(song + '/')}) + self.write(json.dumps({'note': note})) + + +class SongEffectsUpdates(cyclone.websocket.WebSocketHandler): + + def connectionMade(self, *args, **kwargs): + self.graph = self.settings.graph + self.graph.addHandler(self.updateClient) + + def updateClient(self): + # todo: abort if client is gone + playlist = self.graph.value(showconfig.showUri(), L9['playList']) + songs = list(self.graph.items(playlist)) + out = [] + for s in songs: + out.append({'uri': s, 'label': self.graph.label(s), 'effects': []}) + seen = set() + for n in self.graph.objects(s, L9['note']): + for uri in self.graph.objects(n, L9['effectClass']): + if uri in seen: + continue + seen.add(uri) + out[-1]['effects'].append({ + 'uri': uri, + 'label': self.graph.label(uri) + }) + out[-1]['effects'].sort(key=lambda e: e['uri']) + + self.sendMessage({'songs': out}) + + +class EffectUpdates(cyclone.websocket.WebSocketHandler): + """ + stays alive for the life of the effect page + """ + + def connectionMade(self, *args, **kwargs): + log.info("websocket opened") + self.uri = URIRef(self.get_argument('uri')) + self.sendMessage({'hello': repr(self)}) + + self.graph = self.settings.graph + self.graph.addHandler(self.updateClient) + + def updateClient(self): + # todo: if client has dropped, abort and don't get any more + # graph updates + + # EffectNode knows how to put them in order. Somehow this is + # not triggering an update when the order changes. + en = EffectNode(self.graph, self.uri) + codeLines = [c.code for c in en.codes] + self.sendMessage({'codeLines': codeLines}) + + def connectionLost(self, reason): + log.info("websocket closed") + + def messageReceived(self, message): + 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')) + 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 received on PUT /code") + return + with self.settings.graph.currentState(tripleFilter=(None, L9['effect'], + effect)) as g: + song = next(g.subjects(L9['effect'], effect)) + + 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) + + +class EffectEval(PrettyErrorHandler, cyclone.web.RequestHandler): + + @inlineCallbacks + def get(self): + # return dmx list for that effect + uri = URIRef(self.get_argument('uri')) + response = yield cyclone.httpclient.fetch( + networking.musicPlayer.path('time')) + songTime = json.loads(response.body)['t'] + + node = EffectNode(self.settings.graph, uri) + outSub = node.eval(songTime) + self.write(json.dumps(outSub.get_dmx_list())) + + +# Completely not sure where the effect background loop should +# go. Another process could own it, and get this request repeatedly: +class SongEffectsEval(PrettyErrorHandler, cyclone.web.RequestHandler): + + def get(self): + song = URIRef(self.get_argument('song')) + effects = effectsForSong(self.settings.graph, song) # noqa + raise NotImplementedError + self.write(maxDict(effectDmxDict(e) for e in effects)) # noqa + # return dmx dict for all effects in the song, already combined + + +class App(object): + + def __init__(self, show, outputWhere): + self.show = show + self.outputWhere = outputWhere + self.graph = SyncedGraph(networking.rdfdb.url, "effectEval") + self.graph.initiallySynced.addCallback(self.launch).addErrback( + log.error) + + def launch(self, *args): + log.info('launch') + if self.outputWhere: + self.loop = makeEffectLoop(self.graph, self.outputWhere) + self.loop.startLoop() + + SFH = cyclone.web.StaticFileHandler + self.cycloneApp = cyclone.web.Application(handlers=[ + (r'/()', SFH, { + 'path': 'light9/effecteval', + 'default_filename': 'index.html' + }), + (r'/effect', EffectEdit), + (r'/effect\.js', StaticCoffee, { + 'src': 'light9/effecteval/effect.coffee' + }), + (r'/(effect-components\.html)', SFH, { + 'path': 'light9/effecteval' + }), + (r'/effectUpdates', EffectUpdates), + (r'/code', Code), + (r'/songEffectsUpdates', SongEffectsUpdates), + (r'/effect/eval', EffectEval), + (r'/songEffects', SongEffects), + (r'/songEffects/eval', SongEffectsEval), + metricsRoute(), + ], + debug=True, + graph=self.graph) + reactor.listenTCP(networking.effectEval.port, self.cycloneApp) + log.info("listening on %s" % networking.effectEval.port) + + +if __name__ == "__main__": + parser = optparse.OptionParser() + parser.add_option( + '--show', + help='show URI, like http://light9.bigasterisk.com/show/dance2008', + default=showconfig.showUri()) + parser.add_option("-v", + "--verbose", + action="store_true", + help="logging.DEBUG") + parser.add_option("--twistedlog", + action="store_true", + help="twisted logging") + parser.add_option("--output", metavar="WHERE", help="dmx or leds") + (options, args) = parser.parse_args() + log.setLevel(logging.DEBUG if options.verbose else logging.INFO) + + if not options.show: + raise ValueError("missing --show http://...") + + app = App(URIRef(options.show), options.output) + if options.twistedlog: + from twisted.python import log as twlog + twlog.startLogging(sys.stderr) + reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/attic/fade --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/fade Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,4 @@ +#!/bin/sh +exec pnpm exec vite -c light9/fade/vite.config.ts + + diff -r 623836db99af -r 4556eebe5d73 bin/attic/gobutton --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/gobutton Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,3 @@ +#!/bin/sh +# uri should be set from $LIGHT9_SHOW/config.n3 +exec curl --silent -d '' http://localhost:8040/go diff -r 623836db99af -r 4556eebe5d73 bin/attic/gtk_dnd_demo.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/gtk_dnd_demo.py Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,20 @@ +#!bin/python +import run_local +import gtk +import sys +sys.path.append(".") +from rdflib import URIRef +from light9 import networking +from light9.editchoicegtk import EditChoice, Local +from light9.observable import Observable +from rdfdb.syncedgraph import SyncedGraph + +win = gtk.Window() + +graph = SyncedGraph(networking.rdfdb.url, "gtkdnddemo") + +r1 = URIRef("http://example.com/interestingThing") +v = Observable(r1) +win.add(EditChoice(graph, v)) +win.show_all() +gtk.main() diff -r 623836db99af -r 4556eebe5d73 bin/attic/inputdemo --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/inputdemo Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,65 @@ +#!bin/python +import sys +sys.path.append('/usr/lib/python2.7/dist-packages') # For gtk +from twisted.internet import gtk3reactor +gtk3reactor.install() +from twisted.internet import reactor +from rdflib import URIRef +import optparse, logging, time +from gi.repository import Gtk +from run_local import log +from light9 import networking +from light9 import clientsession +from rdfdb.syncedgraph import SyncedGraph +from light9.curvecalc.client import sendLiveInputPoint + + +class App(object): + + def __init__(self): + parser = optparse.OptionParser() + parser.set_usage("%prog [opts] [curve uri]") + parser.add_option("--debug", action="store_true", help="log at DEBUG") + clientsession.add_option(parser) + opts, args = parser.parse_args() + + log.setLevel(logging.DEBUG if opts.debug else logging.INFO) + + self.session = clientsession.getUri('inputdemo', opts) + self.graph = SyncedGraph(networking.rdfdb.url, "inputdemo") + + self.graph.initiallySynced.addCallback(lambda _: self.launch()) + + self.curve = args[0] if args else URIRef( + 'http://light9.bigasterisk.com/show/dance2014/song1/curve/c-1401259747.675542' + ) + print("sending points on curve %s" % self.curve) + + reactor.run() + + def launch(self): + win = Gtk.Window() + + slider = Gtk.Scale.new_with_range(orientation=Gtk.Orientation.VERTICAL, + min=0, + max=1, + step=.001) + slider.props.inverted = True + slider.connect('value-changed', self.onChanged) + + win.add(slider) + win.parse_geometry('50x250') + win.connect("delete-event", lambda *a: reactor.crash()) + win.connect("destroy", lambda *a: reactor.crash()) + win.show_all() + + def onChanged(self, scale): + t1 = time.time() + d = sendLiveInputPoint(self.curve, scale.get_value()) + + @d.addCallback + def done(result): + print("posted in %.1f ms" % (1000 * (time.time() - t1))) + + +App() diff -r 623836db99af -r 4556eebe5d73 bin/attic/inputquneo --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/inputquneo Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,115 @@ +#!bin/python +""" +read Quneo midi events, write to curvecalc and maybe to effects +""" + +from run_local import log +import logging, urllib.request, urllib.parse, urllib.error +import cyclone.web, cyclone.httpclient +from rdflib import URIRef +from twisted.internet import reactor, task +from light9.curvecalc.client import sendLiveInputPoint +from light9.namespaces import L9, RDF +from rdfdb.syncedgraph import SyncedGraph +from light9 import networking + +import sys +sys.path.append('/usr/lib/python2.7/dist-packages') # For pygame +import pygame.midi + +curves = { + 23: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-2'), + 24: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-3'), + 25: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-4'), + 6: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-5'), + 18: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-6'), +} + + +class WatchMidi: + + def __init__(self, graph): + self.graph = graph + pygame.midi.init() + + dev = self.findQuneo() + self.inp = pygame.midi.Input(dev) + task.LoopingCall(self.step).start(.05) + + self.noteIsOn = {} + + self.effectMap = {} # note: effect class uri + self.graph.addHandler(self.setupNotes) + + def setupNotes(self): + for e in self.graph.subjects(RDF.type, L9['EffectClass']): + qn = self.graph.value(e, L9['quneoNote']) + if qn: + self.effectMap[int(qn)] = e + log.info("setup with %s effects", len(self.effectMap)) + + def findQuneo(self): + for dev in range(pygame.midi.get_count()): + interf, name, isInput, isOutput, opened = pygame.midi.get_device_info( + dev) + if 'QUNEO' in name and isInput: + return dev + raise ValueError("didn't find quneo input device") + + def step(self): + if not self.inp.poll(): + return + NOTEON, NOTEOFF = 144, 128 + for ev in self.inp.read(999): + (status, d1, d2, _), _ = ev + if status in [NOTEON, NOTEOFF]: + print(status, d1, d2) + + if status == NOTEON: + if not self.noteIsOn.get(d1): + self.noteIsOn[d1] = True + try: + e = self.effectMap[d1] + cyclone.httpclient.fetch( + url=networking.effectEval.path('songEffects'), + method='POST', + headers={ + 'Content-Type': + ['application/x-www-form-urlencoded'] + }, + postdata=urllib.parse.urlencode([('drop', e)]), + ) + except KeyError: + pass + + if status == NOTEOFF: + self.noteIsOn[d1] = False + + if 0: + # curve editing mode, not done yet + for group in [(23, 24, 25), (6, 18)]: + if d1 in group: + if not self.noteIsOn.get(group): + print("start zero") + + for d in group: + sendLiveInputPoint(curves[d], 0) + self.noteIsOn[group] = True + else: # miss first update + sendLiveInputPoint(curves[d1], d2 / 127) + + if status == 128: #noteoff + for d in group: + sendLiveInputPoint(curves[d], 0) + self.noteIsOn[group] = False + + +def main(): + log.setLevel(logging.DEBUG) + graph = SyncedGraph(networking.rdfdb.url, "inputQuneo") + wm = WatchMidi(graph) + reactor.run() + del wm + + +main() diff -r 623836db99af -r 4556eebe5d73 bin/attic/kcclient --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/kcclient Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,16 @@ +#!/usr/bin/env python +"""send KeyboardComposer a fade request, for use from the shell""" + +import sys +import run_local +from restclient import Resource +from light9 import networking + +subname = sys.argv[1] +level = sys.argv[2] +fadesecs = '0' +if len(sys.argv) > 3: + fadesecs = sys.argv[3] + +levelServer = Resource(networking.keyboardComposer.url) +levelServer.post('fadesub', subname=subname, level=level, secs=fadesecs) diff -r 623836db99af -r 4556eebe5d73 bin/attic/keyboardcomposer --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/keyboardcomposer Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,712 @@ +#!bin/python + +from run_local import log + +from optparse import OptionParser +from typing import Any, Dict, Tuple, List +import cgi, time, logging +import imp +import tkinter.tix as tk + +from louie import dispatcher +from rdflib import URIRef, Literal +from twisted.internet import reactor, tksupport +from twisted.web import resource +import webcolors, colorsys + +from bcf2000 import BCF2000 +from light9 import clientsession +from light9 import showconfig, networking, prof +from light9.Fadable import Fadable +from light9.effect.sequencer import CodeWatcher +from light9.effect.settings import DeviceSettings +from light9.effect.simple_outputs import SimpleOutputs +from light9.namespaces import L9, RDF, RDFS +from light9.subclient import SubClient +from light9.tkdnd import initTkdnd, dragSourceRegister, dropTargetRegister +from light9.uihelpers import toplevelat +from rdfdb.patch import Patch +from rdfdb.syncedgraph import SyncedGraph +import light9.effect.effecteval + +nudge_keys = {'up': list('qwertyui'), 'down': list('asdfghjk')} + + +class DummySliders: + + def valueOut(self, name, value): + pass + + def close(self): + pass + + def reopen(self): + pass + + +class SubScale(tk.Scale, Fadable): + + def __init__(self, master, *args, **kw): + self.scale_var = kw.get('variable') or tk.DoubleVar() + kw.update({ + 'variable': self.scale_var, + 'from': 1., + 'to': 0., + 'showvalue': 0, + 'sliderlength': 15, + 'res': 0.001, + 'width': 40, + 'troughcolor': 'black', + 'bg': 'grey40', + 'highlightthickness': 1, + 'bd': 1, + 'highlightcolor': 'red', + 'highlightbackground': 'black', + 'activebackground': 'red' + }) + tk.Scale.__init__(self, master, *args, **kw) + Fadable.__init__(self, var=self.scale_var, wheel_step=0.05) + self.draw_indicator_colors() + + def draw_indicator_colors(self): + if self.scale_var.get() == 0: + self['troughcolor'] = 'black' + else: + self['troughcolor'] = 'blue' + + +class SubmasterBox(tk.Frame): + """ + this object owns the level of the submaster (the rdf graph is the + real authority) + + This leaks handlers or DoubleVars or something and tries to just + skip the obsolete ones. It'll get slower and bigger over + time. todo: make aa web version. + """ + + def __init__(self, master, graph, sub, session, col, row): + self.graph = graph + self.sub = sub + self.session = session + self.col, self.row = col, row + bg = self.graph.value(sub, L9['color'], default='#000000') + rgb = webcolors.hex_to_rgb(bg) + hsv = colorsys.rgb_to_hsv(*[x / 255 for x in rgb]) + darkBg = webcolors.rgb_to_hex( + tuple([ + int(x * 255) for x in colorsys.hsv_to_rgb(hsv[0], hsv[1], .2) + ])) + tk.Frame.__init__(self, master, bd=1, relief='raised', bg=bg) + self.name = self.graph.label(sub) + self._val = 0.0 + self.slider_var = tk.DoubleVar() + self.pauseTrace = False + self.scale = SubScale(self, variable=self.slider_var, width=20) + self.dead = False + + self.namelabel = tk.Label(self, + font="Arial 9", + bg=darkBg, + fg='white', + pady=0) + self.graph.addHandler(self.updateName) + + self.namelabel.pack(side=tk.TOP) + self.levellabel = tk.Label(self, + textvariable=self.slider_var, + font="Arial 6", + bg='black', + fg='white', + pady=0) + self.levellabel.pack(side=tk.TOP) + self.scale.pack(side=tk.BOTTOM, expand=1, fill=tk.BOTH) + + for w in [self, self.namelabel, self.levellabel]: + dragSourceRegister(w, 'copy', 'text/uri-list', sub) + + self._slider_var_trace = self.slider_var.trace('w', self.slider_changed) + + self.graph.addHandler(self.updateLevelFromGraph) + + # initial position + # stil need? dispatcher.send("send_to_hw", sub=sub.uri, hwCol=col + 1) + + def getVal(self) -> float: + return self._val + + def setVal(self, newVal: float) -> None: + if self.dead: + return + try: + self.scale.set(newVal) + self.levellabel.config(text=str(newVal)) + except Exception: + log.warning("disabling handlers on broken subbox") + self.dead = True + + def cleanup(self): + self.slider_var.trace_vdelete('w', self._slider_var_trace) + + def slider_changed(self, *args): + self._val = self.scale.get() + self.scale.draw_indicator_colors() + + if self.pauseTrace: + return + self.updateGraphWithLevel(self.sub, self.getVal()) + + # needs fixing: plan is to use dispatcher or a method call to tell a hardware-mapping object who changed, and then it can make io if that's a current hw slider + dispatcher.send("send_to_hw", + sub=self.sub, + hwCol=self.col + 1, + boxRow=self.row) + + def updateGraphWithLevel(self, uri, level): + """in our per-session graph, we maintain SubSetting objects like this: + + ?session :subSetting [a :SubSetting; :sub ?s; :level ?l] + """ + # move to syncedgraph patchMapping + + self.graph.patchMapping(context=self.session, + subject=self.session, + predicate=L9['subSetting'], + nodeClass=L9['SubSetting'], + keyPred=L9['sub'], + newKey=uri, + valuePred=L9['level'], + newValue=Literal(level)) + + def updateLevelFromGraph(self): + """read rdf level, write it to subbox.slider_var""" + # move this to syncedgraph readMapping + graph = self.graph + + for setting in graph.objects(self.session, L9['subSetting']): + if graph.value(setting, L9['sub']) == self.sub: + self.pauseTrace = True # don't bounce this update back to server + try: + self.setVal(graph.value(setting, L9['level']).toPython()) + finally: + self.pauseTrace = False + + def updateName(self): + if self.scale is None: + return + + def shortUri(u): + return '.../' + u.split('/')[-1] + + try: + self.namelabel.config( + text=self.graph.label(self.sub) or shortUri(self.sub)) + except Exception: + log.warn("disabling handlers on broken subbox") + self.scale = None + + +class KeyboardComposer(tk.Frame, SubClient): + + def __init__(self, + root: tk.Tk, + graph: SyncedGraph, + session: URIRef, + hw_sliders=True): + tk.Frame.__init__(self, root, bg='black') + SubClient.__init__(self) + self.graph = graph + self.session = session + + self.subbox: Dict[URIRef, SubmasterBox] = {} # sub uri : SubmasterBox + self.slider_table: Dict[Tuple[int, int], SubmasterBox] = { + } # coords : SubmasterBox + self.rows: List[tk.Frame] = [] # this holds Tk Frames for each row + + self.current_row = 0 # should come from session graph + + self.use_hw_sliders = hw_sliders + self.connect_to_hw(hw_sliders) + + self.make_key_hints() + self.make_buttons() + + self.graph.addHandler(self.redraw_sliders) + + self.codeWatcher = CodeWatcher( + onChange=lambda: self.graph.addHandler(self.redraw_sliders)) + + self.send_levels_loop(periodSec=.05) + self.graph.addHandler(self.rowFromGraph) + + def make_buttons(self): + self.buttonframe = tk.Frame(self, bg='black') + self.buttonframe.pack(side=tk.BOTTOM) + + self.sliders_status_var = tk.IntVar() + self.sliders_status_var.set(self.use_hw_sliders) + self.sliders_checkbutton = tk.Checkbutton( + self.buttonframe, + text="Sliders", + variable=self.sliders_status_var, + command=lambda: self.toggle_slider_connectedness(), + bg='black', + fg='white') + self.sliders_checkbutton.pack(side=tk.LEFT) + + self.alltozerobutton = tk.Button(self.buttonframe, + text="All to Zero", + command=self.alltozero, + bg='black', + fg='white') + self.alltozerobutton.pack(side='left') + + self.save_stage_button = tk.Button( + self.buttonframe, + text="Save", + command=lambda: self.save_current_stage(self.sub_name.get()), + bg='black', + fg='white') + self.save_stage_button.pack(side=tk.LEFT) + self.sub_name = tk.Entry(self.buttonframe, bg='black', fg='white') + self.sub_name.pack(side=tk.LEFT) + + def redraw_sliders(self) -> None: + self.draw_sliders() + if len(self.rows): + self.change_row(self.current_row) + self.rows[self.current_row].focus() + + self.stop_frequent_update_time = 0 + + def draw_sliders(self): + for r in self.rows: + r.destroy() + self.rows = [] + for b in list(self.subbox.values()): + b.cleanup() + self.subbox.clear() + self.slider_table.clear() + + self.tk_focusFollowsMouse() + + rowcount = -1 + col = 0 + last_group = None + + withgroups = [] + for effect in self.graph.subjects(RDF.type, L9['Effect']): + withgroups.append((self.graph.value(effect, L9['group']), + self.graph.value(effect, L9['order']), + self.graph.label(effect), effect)) + withgroups.sort() + + log.debug("withgroups %s", withgroups) + + self.effectEval: Dict[URIRef, light9.effect.effecteval.EffectEval] = {} + imp.reload(light9.effect.effecteval) + simpleOutputs = SimpleOutputs(self.graph) + for group, order, sortLabel, effect in withgroups: + if col == 0 or group != last_group: + row = self.make_row(group) + rowcount += 1 + col = 0 + + subbox = SubmasterBox(row, self.graph, effect, self.session, col, + rowcount) + subbox.place(relx=col / 8, rely=0, relwidth=1 / 8, relheight=1) + self.subbox[effect] = self.slider_table[(rowcount, col)] = subbox + + self.setup_key_nudgers(subbox.scale) + + self.effectEval[effect] = light9.effect.effecteval.EffectEval( + self.graph, effect, simpleOutputs) + + col = (col + 1) % 8 + last_group = group + + def toggle_slider_connectedness(self): + self.use_hw_sliders = not self.use_hw_sliders + if self.use_hw_sliders: + self.sliders.reopen() + else: + self.sliders.close() + self.change_row(self.current_row) + self.rows[self.current_row].focus() + + def connect_to_hw(self, hw_sliders): + log.info('connect_to_hw') + if hw_sliders: + try: + self.sliders = Sliders(self) + log.info("connected to sliders") + except IOError as e: + log.info("no hardware sliders %r", e) + self.sliders = DummySliders() + self.use_hw_sliders = False + dispatcher.connect(self.send_to_hw, 'send_to_hw') + else: + self.sliders = DummySliders() + + def make_key_hints(self): + keyhintrow = tk.Frame(self) + + col = 0 + for upkey, downkey in zip(nudge_keys['up'], nudge_keys['down']): + # what a hack! + downkey = downkey.replace('semicolon', ';') + upkey, downkey = (upkey.upper(), downkey.upper()) + + # another what a hack! + keylabel = tk.Label(keyhintrow, + text='%s\n%s' % (upkey, downkey), + width=1, + font=('Arial', 10), + bg='red', + fg='white', + anchor='c') + keylabel.pack(side=tk.LEFT, expand=1, fill=tk.X) + col += 1 + + keyhintrow.pack(fill=tk.X, expand=0) + self.keyhints = keyhintrow + + def setup_key_nudgers(self, tkobject): + for d, keys in list(nudge_keys.items()): + for key in keys: + # lowercase makes full=0 + keysym = "" % key + tkobject.bind(keysym, + lambda evt, num=keys.index(key), d=d: self. + got_nudger(num, d)) + + # uppercase makes full=1 + keysym = "" % key.upper() + keysym = keysym.replace('SEMICOLON', 'colon') + tkobject.bind(keysym, + lambda evt, num=keys.index(key), d=d: self. + got_nudger(num, d, full=1)) + + # Row changing: + # Page dn, C-n, and ] do down + # Page up, C-p, and ' do up + for key in ' ' \ + ' '.split(): + tkobject.bind(key, self.change_row_cb) + + def change_row_cb(self, event): + diff = 1 + if event.keysym in ('Prior', 'p', 'bracketright'): + diff = -1 + self.change_row(self.current_row + diff) + + def rowFromGraph(self): + self.change_row(int( + self.graph.value(self.session, L9['currentRow'], default=0)), + fromGraph=True) + + def change_row(self, row: int, fromGraph=False) -> None: + old_row = self.current_row + self.current_row = row + self.current_row = max(0, self.current_row) + self.current_row = min(len(self.rows) - 1, self.current_row) + try: + row = self.rows[self.current_row] + except IndexError: + # if we're mid-load, this row might still appear soon. If + # we changed interactively, the user is out of bounds and + # needs to be brought back in + if fromGraph: + return + raise + + self.unhighlight_row(old_row) + self.highlight_row(self.current_row) + self.keyhints.pack_configure(before=row) + + if not fromGraph: + self.graph.patchObject(self.session, self.session, L9['currentRow'], + Literal(self.current_row)) + + for col in range(1, 9): + try: + subbox = self.slider_table[(self.current_row, col - 1)] + self.sliders.valueOut("button-upper%d" % col, True) + except KeyError: + # unfilled bottom row has holes (plus rows with incomplete + # groups + self.sliders.valueOut("button-upper%d" % col, False) + self.sliders.valueOut("slider%d" % col, 0) + continue + self.send_to_hw(sub=subbox.sub, hwCol=col, boxRow=self.current_row) + + def got_nudger(self, number, direction, full=0): + try: + subbox = self.slider_table[(self.current_row, number)] + except KeyError: + return + + if direction == 'up': + if full: + subbox.scale.fade(1) + else: + subbox.scale.increase() + else: + if full: + subbox.scale.fade(0) + else: + subbox.scale.decrease() + + def hw_slider_moved(self, col, value): + value = int(value * 100) / 100 + try: + subbox = self.slider_table[(self.current_row, col)] + except KeyError: + return # no slider assigned at that column + + if hasattr(self, 'pendingHwSet'): + import twisted.internet.error + try: + self.pendingHwSet.cancel() + except twisted.internet.error.AlreadyCalled: + pass + self.pendingHwSet = reactor.callLater(.01, subbox.setVal, value) + + def send_to_hw(self, sub, hwCol, boxRow): + if isinstance(self.sliders, DummySliders): + return + + assert isinstance(sub, URIRef), repr(sub) + + if boxRow != self.current_row: + return + + try: + level = self.get_levels()[sub] + except KeyError: + log.warn("%r not in %r", sub, self.get_levels()) + raise + v = round(127 * level) + chan = "slider%s" % hwCol + + # workaround for some rounding issue, where we receive one + # value and then decide to send back a value that's one step + # lower. -5 is a fallback for having no last value. hopefully + # we won't really see it + if abs(v - self.sliders.lastValue.get(chan, -5)) <= 1: + return + self.sliders.valueOut(chan, v) + + def make_row(self, group): + """group is a URI or None""" + row = tk.Frame(self, bd=2, bg='black') + row.subGroup = group + + def onDrop(ev): + self.change_group(sub=URIRef(ev.data), row=row) + return "link" + + dropTargetRegister(row, + onDrop=onDrop, + typeList=['*'], + hoverStyle=dict(background="#555500")) + + row.pack(expand=1, fill=tk.BOTH) + self.setup_key_nudgers(row) + self.rows.append(row) + return row + + def change_group(self, sub, row): + """update this sub's group, and maybe other sub groups as needed, so + this sub displays in this row""" + group = row.subGroup + self.graph.patchObject(context=self.session, + subject=sub, + predicate=L9['group'], + newObject=group) + + def highlight_row(self, row): + row = self.rows[row] + row['bg'] = 'red' + + def unhighlight_row(self, row): + row = self.rows[row] + row['bg'] = 'black' + + def get_levels(self): + return dict([ + (uri, box.getVal()) for uri, box in list(self.subbox.items()) + ]) + + def get_output_settings(self, _graph=None): + _graph = _graph or self.graph + outputSettings = [] + for setting in _graph.objects(self.session, L9['subSetting']): + effect = _graph.value(setting, L9['sub']) + strength = _graph.value(setting, L9['level']) + if strength: + now = time.time() + out, report = self.effectEval[effect].outputFromEffect( + [(L9['strength'], strength)], + songTime=now, + # should be counting from when you bumped up from 0 + noteTime=now) + outputSettings.append(out) + + return DeviceSettings.fromList(_graph, outputSettings) + + def save_current_stage(self, subname): + log.info("saving current levels as %s", subname) + with self.graph.currentState() as g: + ds = self.get_output_settings(_graph=g) + effect = L9['effect/%s' % subname] + ctx = URIRef(showconfig.showUri() + '/effect/' + subname) + stmts = ds.statements(effect, ctx, effect + '/', set()) + stmts.extend([ + (effect, RDF.type, L9['Effect'], ctx), + (effect, RDFS.label, Literal(subname), ctx), + (effect, L9['publishAttr'], L9['strength'], ctx), + ]) + + self.graph.suggestPrefixes(ctx, {'eff': effect + '/'}) + self.graph.patch(Patch(addQuads=stmts, delQuads=[])) + + self.sub_name.delete(0, tk.END) + + def alltozero(self): + for uri, subbox in list(self.subbox.items()): + if subbox.scale.scale_var.get() != 0: + subbox.scale.fade(value=0.0, length=0) + + +# move to web lib +def postArgGetter(request): + """return a function that takes arg names and returns string + values. Supports args encoded in the url or in postdata. No + support for repeated args.""" + # this is something nevow normally does for me + request.content.seek(0) + fields = cgi.FieldStorage(request.content, + request.received_headers, + environ={'REQUEST_METHOD': 'POST'}) + + def getArg(n): + try: + return request.args[n][0] + except KeyError: + return fields[n].value + + return getArg + + +class LevelServerHttp(resource.Resource): + isLeaf = True + + def __init__(self, name_to_subbox): + self.name_to_subbox = name_to_subbox + + def render_POST(self, request): + arg = postArgGetter(request) + + if request.path == '/fadesub': + # fadesub?subname=scoop&level=0&secs=.2 + self.name_to_subbox[arg('subname')].scale.fade( + float(arg('level')), float(arg('secs'))) + return "set %s to %s" % (arg('subname'), arg('level')) + else: + raise NotImplementedError(repr(request)) + + +class Sliders(BCF2000): + + def __init__(self, kc): + devices = [ + '/dev/snd/midiC3D0', '/dev/snd/midiC2D0', '/dev/snd/midiC1D0' + ] + for dev in devices: + try: + log.info('try sliders on %s', dev) + BCF2000.__init__(self, dev=dev) + except IOError: + if dev is devices[-1]: + raise + else: + break + + self.kc = kc + log.info('found sliders on %s', dev) + + def valueIn(self, name, value): + kc = self.kc + if name.startswith("slider"): + kc.hw_slider_moved(int(name[6:]) - 1, value / 127) + elif name.startswith("button-upper"): + kc.change_row(kc.current_row) + elif name.startswith("button-lower"): + col = int(name[12:]) - 1 + self.valueOut(name, 0) + try: + tkslider = kc.slider_table[(kc.current_row, col)] + except KeyError: + return + + if tkslider.getVal() == 1.0: + tkslider.setVal(0.0) + else: + tkslider.setVal(1.0) + elif name.startswith("button-corner"): + button_num = int(name[13:]) - 1 + if button_num == 1: + diff = -1 + elif button_num == 3: + diff = 1 + else: + return + + kc.change_row(kc.current_row + diff) + self.valueOut(name, 0) + + +def launch(opts: Any, root: tk.Tk, graph: SyncedGraph, session: URIRef): + tl = toplevelat("Keyboard Composer - %s" % opts.session, + existingtoplevel=root, + graph=graph, + session=session) + + kc = KeyboardComposer(tl, graph, session, hw_sliders=not opts.no_sliders) + kc.pack(fill=tk.BOTH, expand=1) + + for helpline in ["Bindings: B3 mute"]: + tk.Label(root, text=helpline, font="Helvetica -12 italic", + anchor='w').pack(side='top', fill='x') + + +if __name__ == "__main__": + parser = OptionParser() + parser.add_option('--no-sliders', + action='store_true', + help="don't attach to hardware sliders") + clientsession.add_option(parser) + parser.add_option('-v', action='store_true', help="log info level") + opts, args = parser.parse_args() + + log.setLevel(logging.DEBUG if opts.v else logging.INFO) + logging.getLogger('colormath').setLevel(logging.INFO) + + graph = SyncedGraph(networking.rdfdb.url, "keyboardcomposer") + + # i think this also needs delayed start (like subcomposer has), to have a valid graph + # before setting any stuff from the ui + + root = tk.Tk() + initTkdnd(root.tk, 'tkdnd/trunk/') + + session = clientsession.getUri('keyboardcomposer', opts) + + graph.initiallySynced.addCallback(lambda _: launch(opts, root, graph, + session)) + + root.protocol('WM_DELETE_WINDOW', reactor.stop) + + tksupport.install(root, ms=20) + prof.run(reactor.run, profile=None) diff -r 623836db99af -r 4556eebe5d73 bin/attic/lightsim --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/lightsim Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,138 @@ +#!bin/python + +import run_local +import sys, logging + +sys.path.append("lib") +import qt4reactor +qt4reactor.install() + +from twisted.internet import reactor +from twisted.internet.task import LoopingCall +from twisted.web.xmlrpc import Proxy +from louie import dispatcher +from PyQt4.QtGui import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QMainWindow +from OpenGL.GL import * +from OpenGL.GLU import * + +from light9 import networking, Patch, showconfig, dmxclient, updatefreq, prof +from light9.namespaces import L9 +from lightsim.openglsim import Surface + +log = logging.getLogger() +logging.basicConfig( + format= + "%(asctime)s %(levelname)-5s %(name)s %(filename)s:%(lineno)d: %(message)s") +log.setLevel(logging.DEBUG) + + +def filenamesForChan(graph, chan): + for lyr in graph.objects(chan, L9['previewLayer']): + for imgPath in graph.objects(lyr, L9['path']): + yield imgPath + + +_lastLevels = None + + +def poll(graph, serv, pollFreq, oglSurface): + pollFreq.update() + dispatcher.send("status", key="pollFreq", value=str(pollFreq)) + d = serv.callRemote("currentlevels", dmxclient._id) + + def received(dmxLevels): + global _lastLevels + if dmxLevels == _lastLevels: + return + _lastLevels = dmxLevels + + level = {} # filename : level + for i, lev in enumerate(dmxLevels): + if lev == 0: + continue + + try: + chan = Patch.get_channel_uri(Patch.get_channel_name(i + 1)) + except KeyError: + continue + + for imgPath in filenamesForChan(graph, chan): + level[str(imgPath)] = lev + + oglSurface.newLevels(levels=level) + + d.addCallback(received) + return d + + +class StatusKeys(QWidget): + """listens for dispatcher signal 'status' and displays the key/value args""" + + def __init__(self, parent): + QWidget.__init__(self) + self.layout = QVBoxLayout() + self.setLayout(self.layout) + self.row = {} # key name : (Frame, value Label) + dispatcher.connect(self.status, "status") + + def status(self, key, value): + if key not in self.row: + row = QWidget() + self.layout.addWidget(row) + cols = QHBoxLayout() + row.setLayout(cols) + lab1 = QLabel(key) + lab2 = QLabel(value) + cols.addWidget(lab1) + cols.addWidget(lab2) + self.row[key] = lab2 + else: + lab = self.row[key] + lab.setText(value) + + +class Window(QMainWindow): + + def __init__(self, filenames): + QMainWindow.__init__(self, None) + self.setWindowTitle(dmxclient._id) + + w = QWidget() + self.setCentralWidget(w) + mainLayout = QVBoxLayout() + w.setLayout(mainLayout) + + self.glWidget = Surface(self, filenames, imgRescaleTo=128 * 2) + + mainLayout.addWidget(self.glWidget) + + status = StatusKeys(mainLayout) + mainLayout.addWidget(status) + + +def requiredImages(graph): + """filenames that we'll need to show, based on a config structure + like this: + ch:frontLeft a :Channel; + :previewLayer [ :path "lightsim/skyline/front-left.png" ] . + """ + filenames = [] + for lyr in graph.objects(None, L9['previewLayer']): + for p in graph.objects(lyr, L9['path']): + filenames.append(str(p)) + return filenames + + +if __name__ == '__main__': + app = reactor.qApp + + graph = showconfig.getGraph() + + window = Window(requiredImages(graph)) + window.show() + + serv = Proxy(networking.dmxServer.url) + pollFreq = updatefreq.Updatefreq() + LoopingCall(poll, graph, serv, pollFreq, window.glWidget).start(.05) + + reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/attic/listsongs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/listsongs Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,28 @@ +#!bin/python +"""for completion, print the available song uris on stdout + +in .zshrc: + +function _songs { local expl; _description files expl 'songs'; compadd "$expl[@]" - `${LIGHT9_SHOW}/../../bin/listsongs` } +compdef _songs curvecalc +""" + +from run_local import log # noqa +from twisted.internet import reactor +from rdflib import RDF +from light9 import networking +from light9.namespaces import L9 +from rdfdb.syncedgraph import SyncedGraph + +graph = SyncedGraph(networking.rdfdb.url, "listsongs") + + +@graph.initiallySynced.addCallback +def printSongs(result): + with graph.currentState() as current: + for song in current.subjects(RDF.type, L9['Song']): + print(song) + reactor.stop() + + +reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/attic/live --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/live Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,2 @@ +#!/bin/zsh +exec pnpm exec vite -c light9/live/vite.config.ts diff -r 623836db99af -r 4556eebe5d73 bin/attic/load_test_rdfdb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/load_test_rdfdb Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,40 @@ +#!bin/python +from run_local import log +from twisted.internet import reactor, task, defer +from rdflib import URIRef, Literal +from twisted.internet.defer import ensureDeferred +from rdfdb.syncedgraph import SyncedGraph +import time, logging + +from light9 import networking, showconfig +from light9.namespaces import L9 + + +class BusyClient: + + def __init__(self, subj, rate): + self.subj = subj + self.rate = rate + + self.graph = SyncedGraph(networking.rdfdb.url, "collector") + self.graph.initiallySynced.addCallback(self.go) + + def go(self, _): + task.LoopingCall(self.loop).start(1 / self.rate) + + def loop(self): + self.graph.patchObject(showconfig.showUri() + '/loadTestContext', + subject=self.subj, + predicate=L9['time'], + newObject=Literal(str(time.time()))) + + +def main(): + log.setLevel(logging.INFO) + + clients = [BusyClient(L9['loadTest_%d' % i], 20) for i in range(10)] + reactor.run() + + +if __name__ == "__main__": + main() diff -r 623836db99af -r 4556eebe5d73 bin/attic/midifade --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/midifade Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,4 @@ +#!/bin/sh +exec pdm run python light9/midifade/midifade.py + + diff -r 623836db99af -r 4556eebe5d73 bin/attic/movesinks --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/movesinks Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,14 @@ +#!/bin/bash + +# from http://askubuntu.com/questions/71863/how-to-change-pulseaudio-sink-with-pacmd-set-default-sink-during-playback/113322#113322 + +echo "Setting default sink to: $1"; +pacmd set-default-sink $1 +pacmd list-sink-inputs | grep index | while read line +do +echo "Moving input: "; +echo $line | cut -f2 -d' '; +echo "to sink: $1"; +pacmd move-sink-input `echo $line | cut -f2 -d' '` $1 + +done diff -r 623836db99af -r 4556eebe5d73 bin/attic/mpd_timing_test --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/mpd_timing_test Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,20 @@ +#!/usr/bin/python +""" +records times coming out of ascoltami + +for example: + + % mpd_timing_test > timing + # play some music in ascoltami, then ctrl-c + % gnuplot + > plot "timing" with lines + +""" + +import xmlrpc.client, time + +s = xmlrpc.client.ServerProxy("http://localhost:8040") +start = time.time() +while True: + print(time.time() - start, s.gettime()) + time.sleep(.01) diff -r 623836db99af -r 4556eebe5d73 bin/attic/musictime --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/musictime Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,53 @@ +#!bin/python +import run_local # noqa +import light9.networking + +import tkinter as tk +from twisted.internet import reactor, tksupport, task + +from light9.ascoltami.musictime_client import MusicTime + +mt = MusicTime() + + +class MusicTimeTk(tk.Frame, MusicTime): + + def __init__(self, master, url): + tk.Frame.__init__(self) + MusicTime.__init__(self, url) + self.timevar = tk.DoubleVar() + self.timelabel = tk.Label(self, + textvariable=self.timevar, + bd=2, + relief='raised', + width=10, + padx=2, + pady=2, + anchor='w') + self.timelabel.pack(expand=1, fill='both') + + def print_time(evt, *args): + self.timevar.set(mt.getLatest().get('t', 0)) + print(self.timevar.get(), evt.keysym) + + self.timelabel.bind('', print_time) + self.timelabel.bind('<1>', print_time) + self.timelabel.focus() + task.LoopingCall(self.update_time).start(.1) + + def update_time(self): + t = self.getLatest().get('t', 0) + self.timevar.set(t) + + +if __name__ == "__main__": + from optparse import OptionParser + parser = OptionParser() + parser.add_option("-u", "--url", default=light9.networking.musicPlayer.url) + options, args = parser.parse_args() + + root = tk.Tk() + root.title("Time") + MusicTimeTk(root, options.url).pack(expand=1, fill='both') + tksupport.install(root, ms=20) + reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/attic/paintserver --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/paintserver Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,126 @@ +#!bin/python + +from run_local import log +import json +from twisted.internet import reactor +from rdfdb.syncedgraph import SyncedGraph +from light9 import networking, showconfig +import optparse, sys, logging +import cyclone.web +from rdflib import URIRef +from light9 import clientsession +import light9.paint.solve +from cycloneerr import PrettyErrorHandler +from light9.namespaces import L9, DEV +from light9.metrics import metrics +import imp + + +class Solve(PrettyErrorHandler, cyclone.web.RequestHandler): + + def post(self): + painting = json.loads(self.request.body) + with metrics('solve').time(): + img = self.settings.solver.draw(painting) + sample, sampleDist = self.settings.solver.bestMatch( + img, device=DEV['aura2']) + with self.settings.graph.currentState() as g: + bestPath = g.value(sample, L9['imagePath']).replace(L9[''], '') + #out = solver.solve(painting) + #layers = solver.simulationLayers(out) + + self.write( + json.dumps({ + 'bestMatch': { + 'uri': sample, + 'path': bestPath, + 'dist': sampleDist + }, + # 'layers': layers, + # 'out': out, + })) + + def reloadSolver(self): + imp.reload(light9.paint.solve) + self.settings.solver = light9.paint.solve.Solver(self.settings.graph) + self.settings.solver.loadSamples() + + +class BestMatches(PrettyErrorHandler, cyclone.web.RequestHandler): + + def post(self): + body = json.loads(self.request.body) + painting = body['painting'] + devs = [URIRef(d) for d in body['devices']] + with metrics('solve').time(): + img = self.settings.solver.draw(painting) + outSettings = self.settings.solver.bestMatches(img, devs) + self.write(json.dumps({'settings': outSettings.asList()})) + + +class App: + + def __init__(self, show, session): + self.show = show + self.session = session + + self.graph = SyncedGraph(networking.rdfdb.url, "paintServer") + self.graph.initiallySynced.addCallback(self.launch).addErrback( + log.error) + + + def launch(self, *args): + + self.solver = light9.paint.solve.Solver( + self.graph, + sessions=[ + L9['show/dance2017/capture/aura1/cap1876596'], + L9['show/dance2017/capture/aura2/cap1876792'], + L9['show/dance2017/capture/aura3/cap1877057'], + L9['show/dance2017/capture/aura4/cap1877241'], + L9['show/dance2017/capture/aura5/cap1877406'], + L9['show/dance2017/capture/q1/cap1874255'], + L9['show/dance2017/capture/q2/cap1873665'], + L9['show/dance2017/capture/q3/cap1876223'], + ]) + self.solver.loadSamples() + + self.cycloneApp = cyclone.web.Application(handlers=[ + (r'/solve', Solve), + (r'/bestMatches', BestMatches), + metricsRoute(), + ], + debug=True, + graph=self.graph, + solver=self.solver) + reactor.listenTCP(networking.paintServer.port, self.cycloneApp) + log.info("listening on %s" % networking.paintServer.port) + + +if __name__ == "__main__": + parser = optparse.OptionParser() + parser.add_option( + '--show', + help='show URI, like http://light9.bigasterisk.com/show/dance2008', + default=showconfig.showUri()) + parser.add_option("-v", + "--verbose", + action="store_true", + help="logging.DEBUG") + parser.add_option("--twistedlog", + action="store_true", + help="twisted logging") + clientsession.add_option(parser) + (options, args) = parser.parse_args() + log.setLevel(logging.DEBUG if options.verbose else logging.INFO) + + if not options.show: + raise ValueError("missing --show http://...") + + session = clientsession.getUri('paint', options) + + app = App(URIRef(options.show), session) + if options.twistedlog: + from twisted.python import log as twlog + twlog.startLogging(sys.stderr) + reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/attic/patchserver --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/patchserver Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,83 @@ +#!bin/python + +from run_local import log + +from rdflib import URIRef +from twisted.internet import reactor +from twisted.internet.defer import inlineCallbacks, Deferred + +import logging +import optparse +import os +import time +import treq +import cyclone.web, cyclone.websocket, cyclone.httpclient + +from cycloneerr import PrettyErrorHandler + +from light9.namespaces import L9, RDF +from light9 import networking, showconfig +from rdfdb.syncedgraph import SyncedGraph + +from light9.effect.settings import DeviceSettings +from rdfdb.patch import Patch +from light9.metrics import metrics, metricsRoute + + + +def launch(graph): + if 0: + reactor.listenTCP( + networking.captureDevice.port, + cyclone.web.Application(handlers=[ + (r'/()', cyclone.web.StaticFileHandler, { + "path": "light9/web", + "default_filename": "patchServer.html" + }), + metricsRoute(), + ]), + interface='::', + ) + log.info('serving http on %s', networking.captureDevice.port) + + def prn(): + width = {} + for dc in graph.subjects(RDF.type, L9['DeviceClass']): + for attr in graph.objects(dc, L9['attr']): + width[dc] = max( + width.get(dc, 0), + graph.value(attr, L9['dmxOffset']).toPython() + 1) + + user = {} # chan: [dev] + for dev in set(graph.subjects(L9['dmxBase'], None)): + dc = graph.value(dev, RDF.type) + base = graph.value(dev, L9['dmxBase']).toPython() + for offset in range(0, width[dc]): + chan = base + offset + user.setdefault(chan, []).append(dev) + + for chan in range(1, max(user) + 1): + dev = user.get(chan, None) + print(f'chan {chan} used by {dev}') + + graph.addHandler(prn) + + +def main(): + parser = optparse.OptionParser() + parser.add_option("-v", + "--verbose", + action="store_true", + help="logging.DEBUG") + (options, args) = parser.parse_args() + log.setLevel(logging.DEBUG if options.verbose else logging.INFO) + + graph = SyncedGraph(networking.rdfdb.url, "captureDevice") + + graph.initiallySynced.addCallback(lambda _: launch(graph)).addErrback( + log.error) + reactor.run() + + +if __name__ == '__main__': + main() diff -r 623836db99af -r 4556eebe5d73 bin/attic/picamserve --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/picamserve Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,204 @@ +#!env_pi/bin/python + +from run_local import log +import sys +sys.path.append('/usr/lib/python2.7/dist-packages/') +import io, logging, traceback, time +import cyclone.web +from twisted.internet import reactor, threads +from twisted.internet.defer import inlineCallbacks +from light9 import prof + +try: + import picamera + cameraCls = picamera.PiCamera +except ImportError: + + class cameraCls(object): + + def __enter__(self): + return self + + def __exit__(self, *a): + pass + + def capture(self, out, *a, **kw): + out.write(open('yuv.demo').read()) + + def capture_continuous(self, *a, **kw): + for i in range(1000): + time.sleep(1) + yield str(i) + + +def setCameraParams(c, arg): + res = int(arg('res', 480)) + c.resolution = { + 480: (640, 480), + 1080: (1920, 1080), + 1944: (2592, 1944), + }[res] + c.shutter_speed = int(arg('shutter', 50000)) + c.exposure_mode = arg('exposure_mode', 'fixedfps') + c.awb_mode = arg('awb_mode', 'off') + c.brightness = int(arg('brightness', 50)) + c.exposure_compensation = int(arg('exposure_compensation', 0)) + c.awb_gains = (float(arg('redgain', 1)), float(arg('bluegain', 1))) + c.ISO = int(arg('iso', 250)) + c.rotation = int(arg('rotation', '0')) + + +def setupCrop(c, arg): + c.crop = (float(arg('x', 0)), float(arg('y', 0)), float(arg('w', 1)), + float(arg('h', 1))) + rw = rh = int(arg('resize', 100)) + # width 1920, showing w=.3 of image, resize=100 -> scale is 100/.3*1920 + # scl is [ output px / camera px ] + scl1 = rw / (c.crop[2] * c.resolution[0]) + scl2 = rh / (c.crop[3] * c.resolution[1]) + if scl1 < scl2: + # width is the constraint; reduce height to the same scale + rh = int(scl1 * c.crop[3] * c.resolution[1]) + else: + # height is the constraint + rw = int(scl2 * c.crop[2] * c.resolution[0]) + return rw, rh + + +@prof.logTime +def getFrame(c, arg): + setCameraParams(c, arg) + resize = setupCrop(c, arg) + out = io.BytesIO('w') + prof.logTime(c.capture)(out, 'jpeg', use_video_port=True, resize=resize) + return out.getvalue() + + +class Pic(cyclone.web.RequestHandler): + + def get(self): + try: + self.set_header('Content-Type', 'image/jpeg') + self.write(getFrame(self.settings.camera, self.get_argument)) + except Exception: + traceback.print_exc() + + +def captureContinuousAsync(c, resize, onFrame): + """ + Calls c.capture_continuous is called in another thread. onFrame is + called in this reactor thread with each (frameTime, frame) + result. Runs until onFrame raises StopIteration. + """ + + def runner(c, resize): + stream = io.BytesIO() + t = time.time() + for nextFrame in c.capture_continuous(stream, + 'jpeg', + use_video_port=True, + resize=resize): + t2 = time.time() + log.debug(" - framecap got %s bytes in %.1f ms", + len(stream.getvalue()), 1000 * (t2 - t)) + try: + # This is slow, like 13ms. Hopefully + # capture_continuous is working on gathering the next + # pic during this time instead of pausing. + # Instead, we could be stashing frames onto a queue or + # something that the main thread can pull when + # possible (and toss if it gets behind). + threads.blockingCallFromThread(reactor, onFrame, t, + stream.getvalue()) + except StopIteration: + break + t3 = time.time() + log.debug(" - sending to onFrame took %.1fms", 1000 * (t3 - t2)) + stream.truncate() + stream.seek(0) + t = time.time() + + return threads.deferToThread(runner, c, resize) + + +class FpsReport(object): + + def __init__(self): + self.frameTimes = [] + self.lastFpsLog = 0 + + def frame(self): + now = time.time() + + self.frameTimes.append(now) + + if len(self.frameTimes) > 15: + del self.frameTimes[:5] + + if now > self.lastFpsLog + 2 and len(self.frameTimes) > 5: + deltas = [(b - a) + for a, b in zip(self.frameTimes[:-1], self.frameTimes[1:]) + ] + avg = sum(deltas) / len(deltas) + log.info("fps: %.1f", 1 / avg) + self.lastFpsLog = now + + +class Pics(cyclone.web.RequestHandler): + + @inlineCallbacks + def get(self): + try: + self.set_header('Content-Type', 'x-application/length-time-jpeg') + c = self.settings.camera + setCameraParams(c, self.get_argument) + resize = setupCrop(c, self.get_argument) + + self.running = True + log.info("connection open from %s", self.request.remote_ip) + fpsReport = FpsReport() + + def onFrame(frameTime, frame): + if not self.running: + raise StopIteration + + self.write("%s %s\n" % (len(frame), frameTime)) + self.write(frame) + self.flush() + + fpsReport.frame() + + # another camera request coming in at the same time breaks + # the server. it would be nice if this request could + # let-go-and-reopen when it knows about another request + # coming in + yield captureContinuousAsync(c, resize, onFrame) + except Exception: + traceback.print_exc() + + def on_connection_close(self, *a, **kw): + log.info("connection closed") + self.running = False + + +log.setLevel(logging.INFO) + +with cameraCls() as camera: + port = 8208 + reactor.listenTCP( + port, + cyclone.web.Application(handlers=[ + (r'/pic', Pic), + (r'/pics', Pics), + (r'/static/(.*)', cyclone.web.StaticFileHandler, { + 'path': 'light9/web/' + }), + (r'/(|gui.js)', cyclone.web.StaticFileHandler, { + 'path': 'light9/vidref/', + 'default_filename': 'index.html' + }), + ], + debug=True, + camera=camera)) + log.info("serving on %s" % port) + reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/attic/pytest --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/pytest Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,2 @@ +#!/bin/sh +exec pdm run pytest "$@" diff -r 623836db99af -r 4556eebe5d73 bin/attic/python --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/python Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,2 @@ +#!/bin/sh +PYTHONPATH=. pdm run python3 "$@" diff -r 623836db99af -r 4556eebe5d73 bin/attic/run_local.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/run_local.py Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,8 @@ +# this file can go away when all the bin/* are just launchers and everyone uses light9/run_local + +import sys + +# to support 'import light9' +sys.path.append('.') + +from light9.run_local import log diff -r 623836db99af -r 4556eebe5d73 bin/attic/staticclient --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/staticclient Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,37 @@ +#!bin/python +""" +push a dmx level forever +""" + +import time, logging +from optparse import OptionParser +import logging, urllib.request, urllib.parse, urllib.error +from twisted.internet import reactor, tksupport, task +from rdflib import URIRef, RDF, RDFS, Literal + +from run_local import log +log.setLevel(logging.DEBUG) + +from light9 import dmxclient, showconfig, networking + +if __name__ == "__main__": + parser = OptionParser(usage="%prog") + parser.add_option('--chan', help='channel number, starts at 1', + type=int) #todo: or name or uri + parser.add_option('--level', help='0..1', type=float) + parser.add_option('-v', action='store_true', help="log debug level") + + opts, args = parser.parse_args() + + log.setLevel(logging.DEBUG if opts.v else logging.INFO) + + levels = [0] * (opts.chan - 1) + [opts.level] + log.info('staticclient will write this forever: %r', levels) + + def write(): + log.debug('writing %r', levels) + dmxclient.outputlevels(levels, twisted=1) + + log.info('looping...') + task.LoopingCall(write).start(1) + reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/attic/subcomposer --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/subcomposer Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,314 @@ +#!bin/python +""" +subcomposer + session + observable(currentSub), a Submaster which tracks the graph + + EditChoice widget + can change currentSub to another sub + + Levelbox widget + watch observable(currentSub) for a new sub, and also watch currentSub for edits to push to the OneLevel widgets + + OneLevel widget + UI edits are caught here and go all the way back to currentSub + + +""" + +from run_local import log +import time, logging + +log.setLevel(logging.DEBUG) + +from optparse import OptionParser +import logging, urllib.request, urllib.parse, urllib.error +import tkinter as tk +import louie as dispatcher +from twisted.internet import reactor, tksupport, task +from rdflib import URIRef, RDF, RDFS, Literal + +from light9.dmxchanedit import Levelbox +from light9 import dmxclient, Submaster, prof, showconfig, networking +from light9.Patch import get_channel_name +from light9.uihelpers import toplevelat +from rdfdb.syncedgraph import SyncedGraph +from light9 import clientsession +from light9.tkdnd import initTkdnd +from light9.namespaces import L9 +from rdfdb.patch import Patch +from light9.observable import Observable +from light9.editchoice import EditChoice, Local +from light9.subcomposer import subcomposerweb + + +class Subcomposer(tk.Frame): + """ + l9:currentSub ?sub is the URI of the sub we're tied to for displaying and + editing. If we don't have a currentSub, then we're actually + editing a session-local sub called l9:currentSub + + I'm not sure that Locals should even be PersistentSubmaster with + uri and graph storage, but I think that way is making fewer + special cases. + + Contains an EditChoice widget + + Dependencies: + + graph (?session :currentSub ?s) -> self.currentSub + self.currentSub -> graph + self.currentSub -> self._currentChoice (which might be Local) + self._currentChoice (which might be Local) -> self.currentSub + + inside the current sub: + graph -> Submaster levels (handled in Submaster) + Submaster levels -> OneLevel widget + OneLevel widget -> Submaster.editLevel + Submaster.editLevel -> graph (handled in Submaster) + + """ + + def __init__(self, master, graph, session): + tk.Frame.__init__(self, master, bg='black') + self.graph = graph + self.session = session + self.launchTime = time.time() + self.localSerial = 0 + + # this is a URIRef or Local. Strangely, the Local case still + # has a uri, which you can get from + # self.currentSub.uri. Probably that should be on the Local + # object too, or maybe Local should be a subclass of URIRef + self._currentChoice = Observable(Local) + + # this is a PersistentSubmaster (even for local) + self.currentSub = Observable( + Submaster.PersistentSubmaster(graph, self.switchToLocal())) + + def pc(val): + log.info("change viewed sub to %s", val) + + self._currentChoice.subscribe(pc) + + ec = self.editChoice = EditChoice(self, self.graph, self._currentChoice) + ec.frame.pack(side='top') + + ec.subIcon.bind("", self.clickSubIcon) + self.setupSubChoiceLinks() + self.setupLevelboxUi() + + def clickSubIcon(self, *args): + box = tk.Toplevel(self.editChoice.frame) + box.wm_transient(self.editChoice.frame) + tk.Label(box, text="Name this sub:").pack() + e = tk.Entry(box) + e.pack() + b = tk.Button(box, text="Make global") + b.pack() + + def clicked(*args): + self.makeGlobal(newName=e.get()) + box.destroy() + + b.bind("", clicked) + e.focus() + + def makeGlobal(self, newName): + """promote our local submaster into a non-local, named one""" + uri = self.currentSub().uri + newUri = showconfig.showUri() + ("/sub/%s" % + urllib.parse.quote(newName, safe='')) + with self.graph.currentState(tripleFilter=(uri, None, None)) as current: + if (uri, RDF.type, L9['LocalSubmaster']) not in current: + raise ValueError("%s is not a local submaster" % uri) + if (newUri, None, None) in current: + raise ValueError("new uri %s is in use" % newUri) + + # the local submaster was storing in ctx=self.session, but now + # we want it to be in ctx=uri + + self.relocateSub(newUri, newName) + + # these are in separate patches for clarity as i'm debugging this + self.graph.patch( + Patch(addQuads=[ + (newUri, RDFS.label, Literal(newName), newUri), + ], + delQuads=[ + (newUri, RDF.type, L9['LocalSubmaster'], newUri), + ])) + self.graph.patchObject(self.session, self.session, L9['currentSub'], + newUri) + + def relocateSub(self, newUri, newName): + # maybe this goes in Submaster + uri = self.currentSub().uri + + def repl(u): + if u == uri: + return newUri + return u + + delQuads = self.currentSub().allQuads() + addQuads = [(repl(s), p, repl(o), newUri) for s, p, o, c in delQuads] + # patch can't span contexts yet + self.graph.patch(Patch(addQuads=addQuads, delQuads=[])) + self.graph.patch(Patch(addQuads=[], delQuads=delQuads)) + + def setupSubChoiceLinks(self): + graph = self.graph + + def ann(): + print("currently: session=%s currentSub=%r _currentChoice=%r" % + (self.session, self.currentSub(), self._currentChoice())) + + @graph.addHandler + def graphChanged(): + # some bug where SC is making tons of graph edits and many + # are failing. this calms things down. + log.warn('skip graphChanged') + return + + s = graph.value(self.session, L9['currentSub']) + log.debug('HANDLER getting session currentSub from graph: %s', s) + if s is None: + s = self.switchToLocal() + self.currentSub(Submaster.PersistentSubmaster(graph, s)) + + @self.currentSub.subscribe + def subChanged(newSub): + log.debug('HANDLER currentSub changed to %s', newSub) + if newSub is None: + graph.patchObject(self.session, self.session, L9['currentSub'], + None) + return + self.sendupdate() + graph.patchObject(self.session, self.session, L9['currentSub'], + newSub.uri) + + localStmt = (newSub.uri, RDF.type, L9['LocalSubmaster']) + with graph.currentState(tripleFilter=localStmt) as current: + if newSub and localStmt in current: + log.debug(' HANDLER set _currentChoice to Local') + self._currentChoice(Local) + else: + # i think right here is the point that the last local + # becomes garbage, and we could clean it up. + log.debug(' HANDLER set _currentChoice to newSub.uri') + self._currentChoice(newSub.uri) + + dispatcher.connect(self.levelsChanged, "sub levels changed") + + @self._currentChoice.subscribe + def choiceChanged(newChoice): + log.debug('HANDLER choiceChanged to %s', newChoice) + if newChoice is Local: + newChoice = self.switchToLocal() + if newChoice is not None: + newSub = Submaster.PersistentSubmaster(graph, newChoice) + log.debug('write new choice to currentSub, from %r to %r', + self.currentSub(), newSub) + self.currentSub(newSub) + + def levelsChanged(self, sub): + if sub == self.currentSub(): + self.sendupdate() + + def switchToLocal(self): + """ + change our display to a local submaster + """ + # todo: where will these get stored, or are they local to this + # subcomposer process and don't use PersistentSubmaster at all? + localId = "%s-%s" % (self.launchTime, self.localSerial) + self.localSerial += 1 + new = URIRef("http://light9.bigasterisk.com/sub/local/%s" % localId) + log.debug('making up a local sub %s', new) + self.graph.patch( + Patch(addQuads=[ + (new, RDF.type, L9['Submaster'], self.session), + (new, RDF.type, L9['LocalSubmaster'], self.session), + ])) + + return new + + def setupLevelboxUi(self): + self.levelbox = Levelbox(self, self.graph, self.currentSub) + self.levelbox.pack(side='top') + + tk.Button( + self, + text="All to zero", + command=lambda *args: self.currentSub().clear()).pack(side='top') + + def savenewsub(self, subname): + leveldict = {} + for i, lev in zip(list(range(len(self.levels))), self.levels): + if lev != 0: + leveldict[get_channel_name(i + 1)] = lev + + s = Submaster.Submaster(subname, levels=leveldict) + s.save() + + def sendupdate(self): + d = self.currentSub().get_dmx_list() + dmxclient.outputlevels(d, twisted=True) + + +def launch(opts, args, root, graph, session): + if not opts.no_geometry: + toplevelat("subcomposer - %s" % opts.session, root, graph, session) + + sc = Subcomposer(root, graph, session) + sc.pack() + + subcomposerweb.init(graph, session, sc.currentSub) + + tk.Label(root, + text="Bindings: B1 adjust level; B2 set full; B3 instant bump", + font="Helvetica -12 italic", + anchor='w').pack(side='top', fill='x') + + if len(args) == 1: + # it might be a little too weird that cmdline arg to this + # process changes anything else in the same session. But also + # I'm not sure who would ever make 2 subcomposers of the same + # session (except when quitting and restarting, to get the + # same window pos), so maybe it doesn't matter. But still, + # this tool should probably default to making new sessions + # usually instead of loading the same one + graph.patchObject(session, session, L9['currentSub'], URIRef(args[0])) + + task.LoopingCall(sc.sendupdate).start(10) + + +############################# + +if __name__ == "__main__": + parser = OptionParser(usage="%prog [suburi]") + parser.add_option('--no-geometry', + action='store_true', + help="don't save/restore window geometry") + parser.add_option('-v', action='store_true', help="log debug level") + + clientsession.add_option(parser) + opts, args = parser.parse_args() + + log.setLevel(logging.DEBUG if opts.v else logging.INFO) + + root = tk.Tk() + root.config(bg='black') + root.tk_setPalette("#004633") + + initTkdnd(root.tk, 'tkdnd/trunk/') + + graph = SyncedGraph(networking.rdfdb.url, "subcomposer") + session = clientsession.getUri('subcomposer', opts) + + graph.initiallySynced.addCallback(lambda _: launch(opts, args, root, graph, + session)) + + root.protocol('WM_DELETE_WINDOW', reactor.stop) + tksupport.install(root, ms=10) + prof.run(reactor.run, profile=False) diff -r 623836db99af -r 4556eebe5d73 bin/attic/subserver --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/subserver Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,104 @@ +#!bin/python +""" +live web display of all existing subs with pictures, mainly for +dragging them into CC or Timeline +""" +from run_local import log +import optparse, logging, json, subprocess, datetime +from dateutil.tz import tzlocal +from twisted.internet import reactor, defer +import cyclone.web, cyclone.httpclient, cyclone.websocket +from rdflib import URIRef, Literal +import pyjade.utils +from rdfdb.syncedgraph import SyncedGraph +from rdfdb.patch import Patch +from light9.namespaces import L9, DCTERMS +from light9 import networking, showconfig + +from cycloneerr import PrettyErrorHandler + + +class Static(PrettyErrorHandler, cyclone.web.StaticFileHandler): + + def get(self, path, *args, **kw): + if path in ['', 'effects']: + return self.respondStaticJade("light9/subserver/%s.jade" % + (path or 'index')) + + if path.endswith(".js"): + return self.responseStaticCoffee( + 'light9/subserver/%s' % + path.replace(".js", ".coffee")) # potential security hole + + cyclone.web.StaticFileHandler.get(self, path, *args, **kw) + + def respondStaticJade(self, src): + html = pyjade.utils.process(open(src).read()) + self.write(html) + + def responseStaticCoffee(self, src): + self.write( + subprocess.check_output( + ['/usr/bin/coffee', '--compile', '--print', src])) + + +class Snapshot(PrettyErrorHandler, cyclone.web.RequestHandler): + + @defer.inlineCallbacks + def post(self): + about = URIRef(self.get_argument("about")) + response = yield cyclone.httpclient.fetch( + networking.vidref.path("snapshot"), method="POST", timeout=1) + + snapUri = URIRef(json.loads(response.body)['snapshot']) + # vidref could write about when it was taken, etc. would it be + # better for us to tell vidref where to attach the result in + # the graph, and then it doesn't even have to return anything? + + ctx = showconfig.showUri() + "/snapshots" + + self.settings.graph.patch( + Patch(addQuads=[ + (about, L9['image'], snapUri, ctx), + (snapUri, DCTERMS['created'], + Literal(datetime.datetime.now(tzlocal())), ctx), + ])) + + self.write(json.dumps({'snapshot': snapUri})) + + +def newestImage(subject): + newest = (None, None) + for img in graph.objects(subject, L9['image']): + created = graph.value(img, DCTERMS['created']) + if created > newest[0]: + newest = (created, img) + return newest[1] + + +if __name__ == "__main__": + parser = optparse.OptionParser() + parser.add_option("-v", + "--verbose", + action="store_true", + help="logging.DEBUG") + (options, args) = parser.parse_args() + + log.setLevel(logging.DEBUG if options.verbose else logging.INFO) + + graph = SyncedGraph(networking.rdfdb.url, "subServer") + + port = networking.subServer.port + reactor.listenTCP( + port, + cyclone.web.Application(handlers=[ + (r'/snapshot', Snapshot), + (r'/(.*)', Static, { + "path": "light9/subserver", + "default_filename": "index.jade" + }), + ], + debug=True, + graph=graph)) + log.info("serving on %s" % port) + reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/attic/timeline --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/timeline Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,3 @@ +#!/bin/zsh +pnpm exec vite -c light9/web/timeline/vite.config.ts & +wait diff -r 623836db99af -r 4556eebe5d73 bin/attic/tkdnd_minimal_drop.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/tkdnd_minimal_drop.py Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,56 @@ +#!bin/python +from run_local import log +import tkinter as tk +from light9.tkdnd import initTkdnd, dropTargetRegister +from twisted.internet import reactor, tksupport + +root = tk.Tk() +initTkdnd(root.tk, "tkdnd/trunk/") +label = tk.Label(root, borderwidth=2, relief='groove', padx=10, pady=10) +label.pack() +label.config(text="drop target %s" % label._w) + +frame1 = tk.Frame() +frame1.pack() + +labelInner = tk.Label(frame1, borderwidth=2, relief='groove', padx=10, pady=10) +labelInner.pack(side='left') +labelInner.config(text="drop target inner %s" % labelInner._w) +tk.Label(frame1, text="not a target").pack(side='left') + + +def onDrop(ev): + print("onDrop", ev) + + +def enter(ev): + print('enter', ev) + + +def leave(ev): + print('leave', ev) + + +dropTargetRegister(label, + onDrop=onDrop, + onDropEnter=enter, + onDropLeave=leave, + hoverStyle=dict(background="yellow", relief='groove')) + +dropTargetRegister(labelInner, + onDrop=onDrop, + onDropEnter=enter, + onDropLeave=leave, + hoverStyle=dict(background="yellow", relief='groove')) + + +def prn(): + print("cont", root.winfo_containing(201, 151)) + + +b = tk.Button(root, text="coord", command=prn) +b.pack() + +#tk.mainloop() +tksupport.install(root, ms=10) +reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/attic/tracker --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/tracker Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,311 @@ +#!/usr/bin/python + +import sys +sys.path.append("../../editor/pour") +sys.path.append("../light8") + +from Submaster import Submaster +from skim.zooming import Zooming, Pair +from math import sqrt, sin, cos +from pygame.rect import Rect +from xmlnodebase import xmlnodeclass, collectiveelement, xmldocfile +from dispatch import dispatcher + +import dmxclient + +import tkinter as tk + +defaultfont = "arial 8" + + +def pairdist(pair1, pair2): + return pair1.dist(pair2) + + +def canvashighlighter(canvas, obj, attribute, normalval, highlightval): + """creates bindings on a canvas obj that make attribute go + from normal to highlight when the mouse is over the obj""" + canvas.tag_bind( + obj, "", lambda ev: canvas.itemconfig( + obj, **{attribute: highlightval})) + canvas.tag_bind( + obj, + "", lambda ev: canvas.itemconfig(obj, **{attribute: normalval})) + + +class Field(xmlnodeclass): + """one light has a field of influence. for any point on the + canvas, you can ask this field how strong it is. """ + + def name(self, newval=None): + """light/sub name""" + return self._getorsetattr("name", newval) + + def center(self, x=None, y=None): + """x,y float coords for the center of this light in the field. returns + a Pair, although it accepts x,y""" + return Pair(self._getorsettypedattr("x", float, x), + self._getorsettypedattr("y", float, y)) + + def falloff(self, dist=None): + """linear falloff from 1 at center, to 0 at dist pixels away + from center""" + return self._getorsettypedattr("falloff", float, dist) + + def getdistforintensity(self, intens): + """returns the distance you'd have to be for the given intensity (0..1)""" + return (1 - intens) * self.falloff() + + def calc(self, x, y): + """returns field strength at point x,y""" + dist = pairdist(Pair(x, y), self.center()) + return max(0, (self.falloff() - dist) / self.falloff()) + + +class Fieldset(collectiveelement): + """group of fields. persistent.""" + + def childtype(self): + return Field + + def version(self): + """read-only version attribute on fieldset tag""" + return self._getorsetattr("version", None) + + def report(self, x, y): + """reports active fields and their intensities""" + active = 0 + for f in self.getall(): + name = f.name() + intens = f.calc(x, y) + if intens > 0: + print(name, intens, end=' ') + active += 1 + if active > 0: + print() + self.dmxsend(x, y) + + def dmxsend(self, x, y): + """output lights to dmx""" + levels = dict([(f.name(), f.calc(x, y)) for f in self.getall()]) + dmxlist = Submaster(None, levels).get_dmx_list() + dmxclient.outputlevels(dmxlist) + + def getbounds(self): + """returns xmin,xmax,ymin,ymax for the non-zero areas of this field""" + r = None + for f in self.getall(): + rad = f.getdistforintensity(0) + fx, fy = f.center() + fieldrect = Rect(fx - rad, fy - rad, rad * 2, rad * 2) + if r is None: + r = fieldrect + else: + r = r.union(fieldrect) + return r.left, r.right, r.top, r.bottom + + +class Fieldsetfile(xmldocfile): + + def __init__(self, filename): + self._openornew(filename, topleveltype=Fieldset) + + def fieldset(self): + return self._gettoplevel() + + +######################################################################## +######################################################################## + + +class FieldDisplay: + """the view for a Field.""" + + def __init__(self, canvas, field): + self.canvas = canvas + self.field = field + self.tags = [str(id(self))] # canvas tag to id our objects + + def setcoords(self): + """adjust canvas obj coords to match the field""" + # this uses the canvas object ids saved by makeobjs + f = self.field + c = self.canvas + w2c = self.canvas.world2canvas + + # rings + for intens, ring in list(self.rings.items()): + rad = f.getdistforintensity(intens) + p1 = w2c(*(f.center() - Pair(rad, rad))) + p2 = w2c(*(f.center() + Pair(rad, rad))) + c.coords(ring, p1[0], p1[1], p2[0], p2[1]) + + # text + p1 = w2c(*f.center()) + c.coords(self.txt, *p1) + + def makeobjs(self): + """(re)create the canvas objs (null coords) and make their bindings""" + c = self.canvas + f = self.field + c.delete(self.tags) + + w2c = self.canvas.world2canvas + + # make rings + self.rings = {} # rad,canvasobj + for intens, color in ( #(1,'white'), + (.8, 'gray90'), (.6, 'gray80'), (.4, 'gray60'), (.2, 'gray50'), + (0, '#000080')): + self.rings[intens] = c.create_oval(0, + 0, + 0, + 0, + outline=color, + width=2, + tags=self.tags, + outlinestipple='gray50') + + # make text + self.txt = c.create_text(0, + 0, + text=f.name(), + font=defaultfont + " bold", + fill='white', + anchor='c', + tags=self.tags) + + # highlight text bindings + canvashighlighter(c, + self.txt, + 'fill', + normalval='white', + highlightval='red') + + # position drag bindings + def press(ev): + self._lastmouse = ev.x, ev.y + + def motion(ev): + dcan = Pair(*[a - b for a, b in zip((ev.x, ev.y), self._lastmouse)]) + dworld = c.canvas2world_vector(*dcan) + self.field.center(*(self.field.center() + dworld)) + self._lastmouse = ev.x, ev.y + self.setcoords() # redraw + + def release(ev): + if hasattr(self, '_lastmouse'): + del self._lastmouse + dispatcher.send("field coord changed") # updates bounds + + c.tag_bind(self.txt, "", press) + c.tag_bind(self.txt, "", motion) + c.tag_bind(self.txt, "", release) + + # radius drag bindings + outerring = self.rings[0] + canvashighlighter(c, + outerring, + 'outline', + normalval='#000080', + highlightval='#4040ff') + + def motion(ev): + worldmouse = self.canvas.canvas2world(ev.x, ev.y) + currentdist = pairdist(worldmouse, self.field.center()) + self.field.falloff(currentdist) + self.setcoords() + + c.tag_bind(outerring, "", motion) + c.tag_bind(outerring, "", release) # from above + + self.setcoords() + + +class Tracker(tk.Frame): + """whole tracker widget, which is mostly a view for a + Fieldset. tracker makes its own fieldset""" + + # world coords of the visible canvas (preserved even in window resizes) + xmin = 0 + xmax = 100 + ymin = 0 + ymax = 100 + + fieldsetfile = None + displays = None # Field : FieldDisplay. we keep these in sync with the fieldset + + def __init__(self, master): + tk.Frame.__init__(self, master) + + self.displays = {} + + c = self.canvas = Zooming(self, bg='black', closeenough=5) + c.pack(fill='both', exp=1) + + # preserve edge coords over window resize + c.bind("", self.configcoords) + + c.bind("", lambda ev: self._fieldset().report(*c.canvas2world( + ev.x, ev.y))) + + def save(ev): + print("saving") + self.fieldsetfile.save() + + master.bind("", save) + dispatcher.connect(self.autobounds, "field coord changed") + + def _fieldset(self): + return self.fieldsetfile.fieldset() + + def load(self, filename): + self.fieldsetfile = Fieldsetfile(filename) + self.displays.clear() + for f in self.fieldsetfile.fieldset().getall(): + self.displays[f] = FieldDisplay(self.canvas, f) + self.displays[f].makeobjs() + self.autobounds() + + def configcoords(self, *args): + # force our canvas coords to stay at the edges of the window + c = self.canvas + cornerx, cornery = c.canvas2world(0, 0) + c.move(cornerx - self.xmin, cornery - self.ymin) + c.setscale(0, 0, + c.winfo_width() / (self.xmax - self.xmin), + c.winfo_height() / (self.ymax - self.ymin)) + + def autobounds(self): + """figure out our bounds from the fieldset, and adjust the display zooms. + writes the corner coords onto the canvas.""" + self.xmin, self.xmax, self.ymin, self.ymax = self._fieldset().getbounds( + ) + + self.configcoords() + + c = self.canvas + c.delete('cornercoords') + for x, anc2 in ((self.xmin, 'w'), (self.xmax, 'e')): + for y, anc1 in ((self.ymin, 'n'), (self.ymax, 's')): + pos = c.world2canvas(x, y) + c.create_text(pos[0], + pos[1], + text="%s,%s" % (x, y), + fill='white', + anchor=anc1 + anc2, + tags='cornercoords') + [d.setcoords() for d in list(self.displays.values())] + + +######################################################################## +######################################################################## + +root = tk.Tk() +root.wm_geometry('700x350') +tra = Tracker(root) +tra.pack(fill='both', exp=1) + +tra.load("fieldsets/demo") + +root.mainloop() diff -r 623836db99af -r 4556eebe5d73 bin/attic/vidref --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/vidref Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,188 @@ +#!bin/python +""" +Camera images of the stage. View live on a web page and also save +them to disk. Retrieve images based on the song and time that was +playing when they were taken. Also, save snapshot images to a place +they can be used again as thumbnails of effects. + +bin/vidref main +light9/vidref/videorecorder.py capture frames and save them +light9/vidref/replay.py backend for vidref.js playback element- figures out which frames go with the current song and time +light9/vidref/index.html web ui for watching current stage and song playback +light9/vidref/setup.html web ui for setup of camera params and frame crop +light9/web/light9-vidref-live.js LitElement for live video frames +light9/web/light9-vidref-playback.js LitElement for video playback + +""" +from run_local import log + +from typing import cast +import logging, optparse, json, base64, os, glob + +from light9.metrics import metrics, metricsRoute + +from rdflib import URIRef +from twisted.internet import reactor, defer +import cyclone.web, cyclone.httpclient, cyclone.websocket + +from cycloneerr import PrettyErrorHandler +from light9 import networking, showconfig +from light9.newtypes import Song +from light9.vidref import videorecorder +from rdfdb.syncedgraph import SyncedGraph + +parser = optparse.OptionParser() +parser.add_option("-v", "--verbose", action="store_true", help="logging.DEBUG") +(options, args) = parser.parse_args() + +log.setLevel(logging.DEBUG if options.verbose else logging.INFO) + + +class Snapshot(cyclone.web.RequestHandler): + + @defer.inlineCallbacks + def post(self): + # save next pic + # return /snapshot/path + try: + snapshotDir = 'todo' + outputFilename = yield self.settings.gui.snapshot() + + assert outputFilename.startswith(snapshotDir) + out = networking.vidref.path( + "snapshot/%s" % outputFilename[len(snapshotDir):].lstrip('/')) + + self.write(json.dumps({'snapshot': out})) + self.set_header("Location", out) + self.set_status(303) + except Exception: + import traceback + traceback.print_exc() + raise + + +pipeline = videorecorder.GstSource( + #'/dev/v4l/by-id/usb-Bison_HD_Webcam_200901010001-video-index0' + '/dev/v4l/by-id/usb-Generic_FULL_HD_1080P_Webcam_200901010001-video-index0') + + +class Live(cyclone.websocket.WebSocketHandler): + + def connectionMade(self, *args, **kwargs): + pipeline.liveImages.subscribe(on_next=self.onFrame) + metrics('live_clients').offset(1) + + def connectionLost(self, reason): + #self.subj.dispose() + metrics('live_clients').offset(-1) + + def onFrame(self, cf: videorecorder.CaptureFrame): + if cf is None: return + + with metrics('live_websocket_frame_fps').time(): + self.sendMessage( + json.dumps({ + 'jpeg': base64.b64encode(cf.asJpeg()).decode('ascii'), + 'description': f't={cf.t}', + })) + + +class SnapshotPic(cyclone.web.StaticFileHandler): + pass + + +class Time(PrettyErrorHandler, cyclone.web.RequestHandler): + + def put(self): + body = json.loads(self.request.body) + t = body['t'] + for listener in TimeStream.time_stream_listeners: + listener.sendMessage(json.dumps({ + 'st': t, + 'song': body['song'], + })) + self.set_status(202) + + +class TimeStream(cyclone.websocket.WebSocketHandler): + time_stream_listeners = [] + + def connectionMade(self, *args, **kwargs): + TimeStream.time_stream_listeners.append(self) + + def connectionLost(self, reason): + TimeStream.time_stream_listeners.remove(self) + + +class Clips(PrettyErrorHandler, cyclone.web.RequestHandler): + + def delete(self): + clip = URIRef(self.get_argument('uri')) + videorecorder.deleteClip(clip) + + +class ReplayMap(PrettyErrorHandler, cyclone.web.RequestHandler): + + def get(self): + song = Song(self.get_argument('song')) + clips = [] + videoPaths = glob.glob( + os.path.join(videorecorder.songDir(song), b'*.mp4')) + for vid in videoPaths: + pts = [] + for line in open(vid.replace(b'.mp4', b'.timing'), 'rb'): + _v, vt, _eq, _song, st = line.split() + pts.append([float(st), float(vt)]) + + url = vid[len(os.path.dirname(os.path.dirname(showconfig.root())) + ):].decode('ascii') + + clips.append({ + 'uri': videorecorder.takeUri(vid), + 'videoUrl': url, + 'songToVideo': pts + }) + + clips.sort(key=lambda c: len(cast(list, c['songToVideo']))) + clips = clips[-int(self.get_argument('maxClips', '3')):] + clips.sort(key=lambda c: c['uri'], reverse=True) + + ret = json.dumps(clips) + log.info('replayMap had %s videos; json is %s bytes', len(clips), + len(ret)) + self.write(ret) + + +graph = SyncedGraph(networking.rdfdb.url, "vidref") +outVideos = videorecorder.FramesToVideoFiles( + pipeline.liveImages, os.path.join(showconfig.root(), b'video')) + +port = networking.vidref.port +reactor.listenTCP( + port, + cyclone.web.Application( + handlers=[ + (r'/()', cyclone.web.StaticFileHandler, { + 'path': 'light9/vidref', + 'default_filename': 'index.html' + }), + (r'/setup/()', cyclone.web.StaticFileHandler, { + 'path': 'light9/vidref', + 'default_filename': 'setup.html' + }), + (r'/live', Live), + (r'/clips', Clips), + (r'/replayMap', ReplayMap), + (r'/snapshot', Snapshot), + (r'/snapshot/(.*)', SnapshotPic, { + "path": 'todo', + }), + (r'/time', Time), + (r'/time/stream', TimeStream), + metricsRoute(), + ], + debug=True, + )) +log.info("serving on %s" % port) + +reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/attic/vidrefsetup --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/vidrefsetup Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,83 @@ +#!bin/python +""" this should be part of vidref, but I haven't worked out sharing +camera captures with a continuous camera capture yet """ + +from run_local import log +import optparse, logging +from twisted.internet import reactor +import cyclone.web, cyclone.httpclient, cyclone.websocket +from rdflib import URIRef +from rdfdb.syncedgraph import SyncedGraph +from light9.namespaces import L9 +from light9 import networking, showconfig + +from cycloneerr import PrettyErrorHandler + + +class RedirToCamera(PrettyErrorHandler, cyclone.web.RequestHandler): + + def get(self): + return self.redirect( + networking.picamserve.path('pic?' + self.request.query)) + + +class UrlToCamera(PrettyErrorHandler, cyclone.web.RequestHandler): + + def get(self): + self.set_header('Content-Type', 'text/plain') + self.write(networking.picamserve.path('pic')) + + +class VidrefCamRequest(PrettyErrorHandler, cyclone.web.RequestHandler): + + def get(self): + graph = self.settings.graph + show = showconfig.showUri() + with graph.currentState(tripleFilter=(show, None, None)) as g: + ret = g.value(show, L9['vidrefCamRequest']) + if ret is None: + self.send_error(404) + self.redirect(ret) + + def put(self): + graph = self.settings.graph + show = showconfig.showUri() + graph.patchObject(context=URIRef(show + '/vidrefConfig'), + subject=show, + predicate=L9['vidrefCamRequest'], + newObject=URIRef(self.get_argument('uri'))) + self.send_error(202) + + +def main(): + parser = optparse.OptionParser() + parser.add_option("-v", + "--verbose", + action="store_true", + help="logging.DEBUG") + (options, args) = parser.parse_args() + + log.setLevel(logging.DEBUG if options.verbose else logging.INFO) + graph = SyncedGraph(networking.rdfdb.url, "vidrefsetup") + + # deliberately conflict with vidref since they can't talk at once to cam + port = networking.vidref.port + + reactor.listenTCP( + port, + cyclone.web.Application(handlers=[ + (r'/pic', RedirToCamera), + (r'/picUrl', UrlToCamera), + (r'/vidrefCamRequest', VidrefCamRequest), + (r'/()', cyclone.web.StaticFileHandler, { + 'path': 'light9/vidref/', + 'default_filename': 'vidref.html' + }), + ], + debug=True, + graph=graph)) + log.info("serving on %s" % port) + reactor.run() + + +main() diff -r 623836db99af -r 4556eebe5d73 bin/attic/wavecurve --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/wavecurve Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,46 @@ +#!bin/python +import optparse +from run_local import log +from light9.wavepoints import simp + + +def createCurve(inpath, outpath, t): + print("reading %s, writing %s" % (inpath, outpath)) + points = simp(inpath.replace('.ogg', '.wav'), seconds_per_average=t) + + f = open(outpath, 'w') + for time_val in points: + print("%s %s" % time_val, file=f) + log.info(r'Wrote {outpath}') + + +parser = optparse.OptionParser(usage="""%prog inputSong.wav outputCurve + +You probably just want -a + +""") +parser.add_option("-t", + type="float", + default=.01, + help="seconds per sample (default .01, .07 is smooth)") +parser.add_option("-a", + "--all", + action="store_true", + help="make standard curves for all songs") +options, args = parser.parse_args() + +if options.all: + from light9 import showconfig + from light9.ascoltami.playlist import Playlist + graph = showconfig.getGraph() + + playlist = Playlist.fromShow(showconfig.getGraph(), showconfig.showUri()) + for song in playlist.allSongs(): + inpath = showconfig.songOnDisk(song) + for curveName, t in [('music', .01), ('smooth_music', .07)]: + outpath = showconfig.curvesDir() + "/%s-%s" % ( + showconfig.songFilenameFromURI(song), curveName) + createCurve(inpath, outpath, t) +else: + inpath, outpath = args + createCurve(inpath, outpath, options.t) diff -r 623836db99af -r 4556eebe5d73 bin/attic/webcontrol --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/attic/webcontrol Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,122 @@ +#!bin/python +""" +web UI for various commands that we might want to run from remote +computers and phones + +todo: +disable buttons that don't make sense +""" +import sys, xmlrpc.client, traceback +from twisted.internet import reactor +from twisted.python import log +from twisted.python.util import sibpath +from twisted.internet.defer import inlineCallbacks, returnValue +from twisted.web.client import getPage +from nevow.appserver import NevowSite +from nevow import rend, static, loaders, inevow, url, tags as T +from rdflib import URIRef +from louie.robustapply import robust_apply +sys.path.append(".") +from light9 import showconfig, networking +from light9.namespaces import L9 +from urllib.parse import urlencode + + +# move to web lib +def post(url, **args): + return getPage(url, method='POST', postdata=urlencode(args)) + + +class Commands(object): + + @staticmethod + def playSong(graph, songUri): + s = xmlrpc.client.ServerProxy(networking.musicPlayer.url) + songPath = graph.value(URIRef(songUri), L9.showPath) + if songPath is None: + raise ValueError("unknown song %s" % songUri) + return s.playfile(songPath.encode('ascii')) + + @staticmethod + def stopMusic(graph): + s = xmlrpc.client.ServerProxy(networking.musicPlayer.url) + return s.stop() + + @staticmethod + def worklightsOn(graph): + return post(networking.keyboardComposer.path('fadesub'), + subname='scoop', + level=.5, + secs=.5) + + @staticmethod + def worklightsOff(graph): + return post(networking.keyboardComposer.path('fadesub'), + subname='scoop', + level=0, + secs=.5) + + @staticmethod + def dimmerSet(graph, dimmer, value): + raise NotImplementedError("subcomposer doesnt have an http port yet") + + +class Main(rend.Page): + docFactory = loaders.xmlfile(sibpath(__file__, "../light9/webcontrol.html")) + + def __init__(self, graph): + self.graph = graph + rend.Page.__init__(self) + + def render_status(self, ctx, data): + pic = T.img(src="icon/enabled.png") + if ctx.arg('error'): + pic = T.img(src="icon/warning.png") + return [pic, ctx.arg('status') or 'ready'] + + def render_songButtons(self, ctx, data): + playList = graph.value(show, L9['playList']) + songs = list(graph.items(playList)) + out = [] + for song in songs: + out.append( + T.form(method="post", action="playSong") + [T.input(type='hidden', name='songUri', value=song), + T.button(type='submit')[graph.label(song)]]) + return out + + @inlineCallbacks + def locateChild(self, ctx, segments): + try: + func = getattr(Commands, segments[0]) + req = inevow.IRequest(ctx) + simpleArgDict = dict((k, v[0]) for k, v in list(req.args.items())) + try: + ret = yield robust_apply(func, func, self.graph, + **simpleArgDict) + except KeyboardInterrupt: + raise + except Exception as e: + print("Error on command %s" % segments[0]) + traceback.print_exc() + returnValue((url.here.up().add('status', + str(e)).add('error', + 1), segments[1:])) + + returnValue((url.here.up().add('status', ret), segments[1:])) + #actually return the orig page, with a status message from the func + except AttributeError: + pass + returnValue(rend.Page.locateChild(self, ctx, segments)) + + def child_icon(self, ctx): + return static.File("/usr/share/pyshared/elisa/plugins/poblesec/tango") + + +graph = showconfig.getGraph() +show = showconfig.showUri() + +log.startLogging(sys.stdout) + +reactor.listenTCP(9000, NevowSite(Main(graph))) +reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/bcf_puppet_demo --- a/bin/bcf_puppet_demo Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,17 +0,0 @@ -#!/usr/bin/python -""" -tiny bcf2000 controller demo -""" -from bcf2000 import BCF2000 -from twisted.internet import reactor - - -class PuppetSliders(BCF2000): - - def valueIn(self, name, value): - if name == 'slider1': - self.valueOut('slider5', value) - - -b = PuppetSliders() -reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/bumppad --- a/bin/bumppad Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,89 +0,0 @@ -#!bin/python - -import sys, time, math -import tkinter as tk - -import run_local -import light9.dmxclient as dmxclient -from light9.TLUtility import make_attributes_from_args - -from light9.Submaster import Submaster, sub_maxes - - -class pad(tk.Frame): - levs = None # Submaster : level - - def __init__(self, master, root, mag): - make_attributes_from_args('master', 'mag') - tk.Frame.__init__(self, master) - self.levs = {} - for xy, key, subname in [ - ((1, 1), 'KP_Up', 'centered'), - ((1, 3), "KP_Down", 'third-c'), - ((0, 2), 'KP_Left', 'scoop-l'), - ((2, 2), 'KP_Right', 'scoop-r'), - ((1, 0), 'KP_Divide', 'cyc'), - ((0, 3), "KP_End", 'hottest'), - ((2, 3), 'KP_Next', 'deepblues'), - ((0, 4), 'KP_Insert', "zip_red"), - ((2, 4), 'KP_Delete', "zip_orange"), - ((3, 1), 'KP_Add', 'strobedim'), - ((3, 3), 'KP_Enter', 'zip_blue'), - ((1, 2), 'KP_Begin', 'scoop-c'), - ]: - - sub = Submaster(subname) - self.levs[sub] = 0 - - l = tk.Label(self, - font="arial 12 bold", - anchor='w', - height=2, - relief='groove', - bd=5, - text="%s\n%s" % (key.replace('KP_', ''), sub.name)) - l.grid(column=xy[0], row=xy[1], sticky='news') - - root.bind( - "" % key, lambda ev, sub=sub: self.bumpto(sub, 1)) - root.bind("" % key, - lambda ev, sub=sub: self.bumpto(sub, 0)) - - def bumpto(self, sub, lev): - now = time.time() - self.levs[sub] = lev * self.mag.get() - self.master.after_idle(self.output) - - def output(self): - dmx = sub_maxes(*[s * l - for s, l in list(self.levs.items())]).get_dmx_list() - dmxclient.outputlevels(dmx, clientid="bumppad") - - -root = tk.Tk() -root.tk_setPalette("maroon4") -root.wm_title("bumppad") -mag = tk.DoubleVar() - -tk.Label(root, - text="Keypad press/release activate sub; 1..5 set mag", - font="Helvetica -12 italic", - anchor='w').pack(side='bottom', fill='x') - -pad(root, root, mag).pack(side='left', fill='both', exp=1) - -magscl = tk.Scale(root, - orient='vertical', - from_=1, - to=0, - res=.01, - showval=1, - variable=mag, - label='mag', - relief='raised', - bd=1) -for i in range(1, 6): - root.bind("" % i, lambda ev, i=i: mag.set(math.sqrt((i) / 5))) -magscl.pack(side='left', fill='y') - -root.mainloop() diff -r 623836db99af -r 4556eebe5d73 bin/captureDevice --- a/bin/captureDevice Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,199 +0,0 @@ -#!bin/python -""" -Operate a motorized light and take pictures of it in every position. -""" -from rdflib import URIRef -from twisted.internet import reactor -from twisted.internet.defer import inlineCallbacks, Deferred - -import logging -import optparse -import os -import time -import treq -import cyclone.web, cyclone.websocket, cyclone.httpclient -from light9.metrics import metrics, metricsRoute -from run_local import log -from cycloneerr import PrettyErrorHandler - -from light9.namespaces import L9, RDF -from light9 import networking, showconfig -from rdfdb.syncedgraph import SyncedGraph -from light9.paint.capture import writeCaptureDescription -from light9.effect.settings import DeviceSettings -from light9.collector.collector_client import sendToCollector -from rdfdb.patch import Patch -from light9.zmqtransport import parseJsonMessage - - - -class Camera(object): - - def __init__(self, imageUrl): - self.imageUrl = imageUrl - - def takePic(self, uri, writePath): - log.info('takePic %s', uri) - return treq.get( - self.imageUrl).addCallbacks(lambda r: self._done(writePath, r), - log.error) - - @inlineCallbacks - def _done(self, writePath, response): - jpg = yield response.content() - try: - os.makedirs(os.path.dirname(writePath)) - except OSError: - pass - with open(writePath, 'w') as out: - out.write(jpg) - log.info('wrote %s', writePath) - - -def deferSleep(sec): - d = Deferred() - reactor.callLater(sec, d.callback, None) - return d - - -class Capture(object): - firstMoveTime = 3 - settleTime = .5 - - def __init__(self, graph, dev): - self.graph = graph - self.dev = dev - - def steps(a, b, n): - return [round(a + (b - a) * i / n, 5) for i in range(n)] - - startTime = time.time() - self.captureId = 'cap%s' % (int(startTime) - 1495170000) - self.toGather = [] - - #quantum - rxSteps = steps(.06, .952, 10) - rySteps = steps(0.1, .77, 5) - zoomSteps = steps(.12, .85, 3) - # aura - rxSteps = steps(0.15, .95, 10) - rySteps = steps(0, .9, 5) - zoomSteps = steps(.6, .9, 3) - - row = 0 - for ry in rySteps: - xSteps = rxSteps[:] - if row % 2: - xSteps.reverse() - row += 1 - for rx in xSteps: - for zoom in zoomSteps: - self.toGather.append( - DeviceSettings( - graph, - [ - (dev, L9['rx'], rx), - (dev, L9['ry'], ry), - (dev, L9['color'], '#ffffff'), - (dev, L9['zoom'], zoom), - #(dev, L9['focus'], 0.13), - ])) - - self.devTail = dev.rsplit('/')[-1] - self.session = URIRef('/'.join( - [showconfig.showUri(), 'capture', self.devTail, self.captureId])) - self.ctx = URIRef(self.session + '/index') - - self.graph.patch( - Patch(addQuads=[ - (self.session, RDF.type, L9['CaptureSession'], self.ctx), - ])) - - self.numPics = 0 - self.settingsCache = set() - self.step().addErrback(log.error) - - def off(self): - return sendToCollector(client='captureDevice', - session='main', - settings=DeviceSettings(self.graph, [])) - - @inlineCallbacks - def step(self): - if not self.toGather: - yield self.off() - yield deferSleep(1) - reactor.stop() - return - settings = self.toGather.pop() - - log.info('[%s left] move to %r', len(self.toGather), settings) - yield sendToCollector(client='captureDevice', - session='main', - settings=settings) - - yield deferSleep(self.firstMoveTime if self.numPics == - 0 else self.settleTime) - - picId = 'pic%s' % self.numPics - path = '/'.join(['capture', self.devTail, self.captureId, picId - ]) + '.jpg' - uri = URIRef(self.session + '/' + picId) - - yield camera.takePic(uri, os.path.join(showconfig.root(), path)) - self.numPics += 1 - - writeCaptureDescription(self.graph, self.ctx, self.session, uri, - self.dev, path, self.settingsCache, settings) - - reactor.callLater(0, self.step) - - -camera = Camera( - 'http://plus:8200/picamserve/pic?res=1080&resize=800&iso=800&redgain=1.6&bluegain=1.6&shutter=60000&x=0&w=1&y=0&h=.952' -) - - -class Attrs(PrettyErrorHandler, cyclone.web.RequestHandler): - - @metrics('set_attr').time() - def put(self): - client, clientSession, settings, sendTime = parseJsonMessage( - self.request.body) - self.set_status(202) - - -def launch(graph): - - cap = Capture(graph, dev=L9['device/aura5']) - reactor.listenTCP(networking.captureDevice.port, - cyclone.web.Application(handlers=[ - (r'/()', cyclone.web.StaticFileHandler, { - "path": "light9/web", - "default_filename": "captureDevice.html" - }), - metricsRoute(), - ]), - interface='::', - cap=cap) - log.info('serving http on %s', networking.captureDevice.port) - - -def main(): - parser = optparse.OptionParser() - parser.add_option("-v", - "--verbose", - action="store_true", - help="logging.DEBUG") - (options, args) = parser.parse_args() - log.setLevel(logging.DEBUG if options.verbose else logging.INFO) - - graph = SyncedGraph(networking.rdfdb.url, "captureDevice") - - graph.initiallySynced.addCallback(lambda _: launch(graph)).addErrback( - log.error) - reactor.run() - - -if __name__ == '__main__': - main() diff -r 623836db99af -r 4556eebe5d73 bin/clientdemo --- a/bin/clientdemo Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,39 +0,0 @@ -#!bin/python - -import os, sys -sys.path.append(".") -from twisted.internet import reactor -import cyclone.web, cyclone.httpclient, logging -from rdflib import Namespace, Literal, URIRef -from light9 import networking -from rdfdb.patch import Patch -from rdfdb.syncedgraph import SyncedGraph - -if __name__ == "__main__": - logging.basicConfig(level=logging.DEBUG) - log = logging.getLogger() - - g = SyncedGraph(networking.rdfdb.url, "clientdemo") - - from light9.Submaster import PersistentSubmaster - sub = PersistentSubmaster( - graph=g, uri=URIRef("http://light9.bigasterisk.com/sub/bcools")) - - #get sub to show its updating name, then push that all the way into KC gui so we can see just names refresh in there - - L9 = Namespace("http://light9.bigasterisk.com/") - - def updateDemoValue(): - v = list(g.objects(L9['demo'], L9['is'])) - print("demo value is %r" % v) - - g.addHandler(updateDemoValue) - - def adj(): - g.patch( - Patch(addQuads=[(L9['demo'], L9['is'], Literal(os.getpid()), - L9['clientdemo'])], - delQuads=[])) - - reactor.callLater(2, adj) - reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/collector_loadtest.py --- a/bin/collector_loadtest.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,52 +0,0 @@ -#!bin/python -import asyncio -import logging -import random -from rdflib import URIRef -import time - -from light9.collector.collector_client_asyncio import sendToCollector -from light9.effect.settings import DeviceSettings -from light9.namespaces import DEV, L9 -from light9.run_local import log - -log.setLevel(logging.DEBUG) - - -async def loadTest(): - print("scheduling loadtest") - n = 200000 - period=.02 - times = [] - session = "loadtest%s" % time.time() - for i in range(n): - if i % 100 == 0: - log.info('sendToCollector %s', i) - start = time.time() - await sendToCollector( - "http://localhost:8202/", - session, - DeviceSettings( - graph=None, - settingsList=[ - # [DEV["backlight1"], L9["color"], "#ffffff"], # - # [DEV["backlight2"], L9["color"], "#ffffff"], - # [DEV["backlight3"], L9["color"], "#ffffff"], - # [DEV["backlight4"], L9["color"], "#ffffff"], - # [DEV["backlight5"], L9["color"], "#ffffff"], - # [DEV["down2"], L9["color"], "#ffffff"], - # [DEV["down3"], L9["color"], "#ffffff"], - # [DEV["down4"], L9["color"], "#ffffff"], - [URIRef('http://light9.bigasterisk.com/theater/skyline/device/down1'), L9["brightness"], random.random()], - [DEV["backlight5"], L9["uv"], 0.011] - ])) - times.append(time.time() - start) - await asyncio.sleep(period) - - print("loadtest done") - with open('/tmp/times', 'w') as f: - f.write(''.join('%s\n' % t for t in times)) - - -if __name__ == '__main__': - asyncio.run(loadTest()) diff -r 623836db99af -r 4556eebe5d73 bin/curvecalc --- a/bin/curvecalc Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,574 +0,0 @@ -#!bin/python -""" -now launches like this: -% bin/curvecalc http://light9.bigasterisk.com/show/dance2007/song1 - - - -todo: curveview should preserve more objects, for speed maybe - -""" - -import sys -import imp -sys.path.append('/usr/lib/python2.7/dist-packages') # For gtk -from twisted.internet import gtk3reactor -gtk3reactor.install() -from twisted.internet import reactor - -import time, textwrap, os, optparse, linecache, signal, traceback, json -import gi -from gi.repository import Gtk -from gi.repository import GObject -from gi.repository import Gdk - -from urllib.parse import parse_qsl -import louie as dispatcher -from rdflib import URIRef, Literal, RDF, RDFS -import logging - -from run_local import log -from light9 import showconfig, networking -from light9.curvecalc import curveview -from light9.curvecalc.curve import Curveset -from light9.curvecalc.curveedit import serveCurveEdit -from light9.curvecalc.musicaccess import Music -from light9.curvecalc.output import Output -from light9.curvecalc.subterm import Subterm -from light9.curvecalc.subtermview import add_one_subterm -from light9.editchoicegtk import EditChoice, Local -from light9.gtkpyconsole import togglePyConsole -from light9.namespaces import L9 -from light9.observable import Observable -from light9 import clientsession -from rdfdb.patch import Patch -from rdfdb.syncedgraph import SyncedGraph -from light9.wavelength import wavelength - - -class SubtermExists(ValueError): - pass - - -class Main(object): - - def __init__(self, graph, opts, session, curveset, music): - self.graph, self.opts, self.session = graph, opts, session - self.curveset, self.music = curveset, music - self.lastSeenInputTime = 0 - self.currentSubterms = [ - ] # Subterm objects that are synced to the graph - - self.setTheme() - wtree = self.wtree = Gtk.Builder() - wtree.add_from_file("light9/curvecalc/curvecalc.glade") - mainwin = wtree.get_object("MainWindow") - - mainwin.connect("destroy", self.onQuit) - wtree.connect_signals(self) - - mainwin.show_all() - - mainwin.connect("delete-event", lambda *args: reactor.crash()) - - def updateTitle(): - mainwin.set_title( - "curvecalc - %s" % - graph.label(graph.value(session, L9['currentSong']))) - - graph.addHandler(updateTitle) - - songChoice = Observable(None) # to be connected with the session song - - self.registerGraphToSongChoice(wtree, session, graph, songChoice) - self.registerSongChoiceToGraph(session, graph, songChoice) - self.registerCurrentPlayerSongToUi(wtree, graph, songChoice) - - ec = EditChoice(graph, songChoice, label="Editing song:") - wtree.get_object("currentSongEditChoice").add(ec) - ec.show() - - wtree.get_object("subterms").connect("add", self.onSubtermChildAdded) - - self.refreshCurveView() - - self.makeStatusLines(wtree.get_object("status")) - self.setupNewSubZone() - self.acceptDragsOnCurveViews() - - # may not work - wtree.get_object("paned1").set_position(600) - - def registerGraphToSongChoice(self, wtree, session, graph, songChoice): - - def setSong(): - current = graph.value(session, L9['currentSong']) - if not wtree.get_object("followPlayerSongChoice").get_active(): - songChoice(current) - dispatcher.send("song_has_changed") - - graph.addHandler(setSong) - - def registerSongChoiceToGraph(self, session, graph, songChoice): - self.muteSongChoiceUntil = 0 - - def songChoiceToGraph(newSong): - if newSong is Local: - raise NotImplementedError('what do i patch') - log.debug('songChoiceToGraph is going to set to %r', newSong) - - # I get bogus newSong values in here sometimes. This - # workaround may not even be helping. - now = time.time() - if now < self.muteSongChoiceUntil: - log.debug('muted') - return - self.muteSongChoiceUntil = now + 1 - - graph.patchObject(context=session, - subject=session, - predicate=L9['currentSong'], - newObject=newSong) - - songChoice.subscribe(songChoiceToGraph) - - def registerCurrentPlayerSongToUi(self, wtree, graph, songChoice): - """current_player_song 'song' param -> playerSong ui - and - current_player_song 'song' param -> songChoice, if you're in autofollow - """ - - def current_player_song(song): - # (this is run on every frame) - ps = wtree.get_object("playerSong") - if URIRef(ps.get_uri()) != song: - log.debug("update playerSong to %s", ps.get_uri()) - - def setLabel(): - ps.set_label(graph.label(song)) - - graph.addHandler(setLabel) - ps.set_uri(song) - if song != songChoice(): - if wtree.get_object("followPlayerSongChoice").get_active(): - log.debug('followPlayerSongChoice is on') - songChoice(song) - - dispatcher.connect(current_player_song, "current_player_song") - self.current_player_song = current_player_song - - def setupNewSubZone(self): - self.wtree.get_object("newSubZone").drag_dest_set( - flags=Gtk.DestDefaults.ALL, - targets=[Gtk.TargetEntry('text/uri-list', 0, 0)], - actions=Gdk.DragAction.COPY) - - def acceptDragsOnCurveViews(self): - w = self.wtree.get_object("curves") - w.drag_dest_set(flags=Gtk.DestDefaults.ALL, - targets=[Gtk.TargetEntry('text/uri-list', 0, 0)], - actions=Gdk.DragAction.COPY) - - def recv(widget, context, x, y, selection, targetType, time): - subUri = URIRef(selection.data.strip()) - print("into curves", subUri) - with self.graph.currentState(tripleFilter=(subUri, RDFS.label, - None)) as current: - subName = current.label(subUri) - - if '?' in subUri: - subName = self.handleSubtermDrop(subUri) - else: - try: - self.makeSubterm(subName, - withCurve=True, - sub=subUri, - expr="%s(t)" % subName) - except SubtermExists: - # we're not making sure the expression/etc are - # correct-- user mihgt need to fix things - pass - curveView = self.curvesetView.row(subName).curveView - t = self.lastSeenInputTime # curveView.current_time() # new curve hasn't heard the time yet. this has gotten too messy- everyone just needs to be able to reach the time source - print("time", t) - curveView.add_points([(t - .5, 0), (t, 1)]) - - w.connect("drag-data-received", recv) - - def onDragDataInNewSubZone(self, widget, context, x, y, selection, - targetType, time): - data = URIRef(selection.data.strip()) - if '?' in data: - self.handleSubtermDrop(data) - return - with self.graph.currentState(tripleFilter=(data, None, - None)) as current: - subName = current.label(data) - self.makeSubterm(newname=subName, - withCurve=True, - sub=data, - expr="%s(t)" % subName) - - def handleSubtermDrop(self, data): - params = parse_qsl(data.split('?')[1]) - flattened = dict(params) - self.makeSubterm(Literal(flattened['subtermName']), - expr=flattened['subtermExpr']) - - for cmd, name in params: - if cmd == 'curve': - self.curveset.new_curve(name) - return name - - def onNewCurve(self, *args): - dialog = self.wtree.get_object("newCurve") - entry = self.wtree.get_object("newCurveName") - # if you don't have songx, that should be the suggested name - entry.set_text("") - if dialog.run() == 1: - self.curveset.new_curve(entry.get_text()) - dialog.hide() - - def onRedrawCurves(self, *args): - dispatcher.send("all curves rebuild") - - def onSubtermsMap(self, *args): - # if this was called too soon, like in __init__, the gtktable - # would get its children but it wouldn't lay anything out that - # I can see, and I'm not sure why. Waiting for map event is - # just a wild guess. - self.graph.addHandler(self.set_subterms_from_graph) - - def onNewSubterm(self, *args): - self.makeSubterm(Literal(""), withCurve=False) - return - - # pretty sure i don't want this back, but not completely sure - # what the UX should be to get the new curve. - - dialog = self.wtree.get_object("newSubterm") - # the plan is to autocomplete this on existing subterm names - # (but let you make one up, too) - entry = self.wtree.get_object("newSubtermName").get_children()[0] - entry.set_text("") - entry.grab_focus() - if dialog.run() == 1: - newname = entry.get_text() - wc = self.wtree.get_object("newSubtermMakeCurve").get_active() - self.makeSubterm(newname, withCurve=wc) - dialog.hide() - - def currentSong(self): - - with self.graph.currentState(tripleFilter=(self.session, - L9['currentSong'], - None)) as current: - return current.value(self.session, L9['currentSong']) - - def songSubtermsContext(self): - return self.currentSong() - - def makeSubterm(self, newname, withCurve=False, expr=None, sub=None): - """ - raises SubtermExists if we had a subterm with a sub with the given - name. what about a no-sub term with the same label? who knows - """ - assert isinstance(newname, Literal), repr(newname) - if withCurve: - self.curveset.new_curve(newname) - if newname in self.all_subterm_labels(): - raise SubtermExists("have a subterm who sub is named %r" % newname) - with self.graph.currentState() as current: - song = self.currentSong() - for i in range(1000): - uri = song + "/subterm/%d" % i - if (uri, None, None) not in current: - break - else: - raise ValueError("can't pick a name for the new subterm") - - ctx = self.songSubtermsContext() - quads = [ - (uri, RDF.type, L9.Subterm, ctx), - (uri, RDFS.label, Literal(newname), ctx), - (self.currentSong(), L9['subterm'], uri, ctx), - ] - if sub is not None: - quads.append((uri, L9['sub'], sub, ctx)) - if expr is not None: - quads.append((uri, L9['expression'], Literal(expr), ctx)) - self.graph.patch(Patch(addQuads=quads)) - - return uri - - def all_subterm_labels(self): - """ - Literal labels of subs in subterms. doesn't currently include labels of the - subterm resources. I'm not sure what I'm going to do with - those. - """ - labels = [] - with self.graph.currentState() as current: - for st in current.objects( - current.value(self.session, L9['currentSong']), - L9['subterm']): - sub = current.value(st, L9['sub']) - if sub is not None: - labels.append(current.label(sub)) - return labels - - def set_subterms_from_graph(self): - """rebuild all the gtktable 'subterms' widgets and the - self.currentSubterms list""" - song = self.graph.value(self.session, L9['currentSong']) - - newList = [] - for st in set(self.graph.objects(song, L9['subterm'])): - log.debug("song %s has subterm %s", song, st) - term = Subterm(self.graph, st, self.songSubtermsContext(), - self.curveset) - newList.append(term) - self.currentSubterms[:] = newList - - master = self.wtree.get_object("subterms") - log.debug("removing subterm widgets") - [master.remove(c) for c in master.get_children()] - for term in self.currentSubterms: - add_one_subterm(term, self.curveset, master) - master.show_all() - log.debug("%s table children showing" % len(master.get_children())) - - def setTheme(self): - settings = Gtk.Settings.get_default() - settings.set_property("gtk-application-prefer-dark-theme", True) - - providers = [] - providers.append(Gtk.CssProvider()) - providers[-1].load_from_path("theme/Just-Dark/gtk-3.0/gtk.css") - providers.append(Gtk.CssProvider()) - providers[-1].load_from_data(''' - * { font-size: 92%; } - .button:link { font-size: 7px } - ''') - - screen = Gdk.Display.get_default_screen(Gdk.Display.get_default()) - for p in providers: - Gtk.StyleContext.add_provider_for_screen( - screen, p, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) - - def onSubtermChildAdded(self, subtermsTable, *args): - # this would probably work, but isn't getting called - log.info("onSubtermChildAdded") - v = subtermsTable.get_parent().props.vadjustment - v.props.value = v.props.upper - - def onQuit(self, *args): - reactor.crash() - # there's a hang after this, maybe in sem_wait in two - # threads. I don't know whose they are. - # This fix affects profilers who want to write output at the end. - os.kill(os.getpid(), signal.SIGKILL) - - def onCollapseAll(self, *args): - self.curvesetView.collapseAll() - - def onCollapseNone(self, *args): - self.curvesetView.collapseNone() - - def onDelete(self, *args): - self.curvesetView.onDelete() - - def onPythonConsole(self, item): - ns = dict() - ns.update(globals()) - ns.update(self.__dict__) - togglePyConsole(self, item, ns) - - def onSeeCurrentTime(self, item): - dispatcher.send("see time") - - def onSeeTimeUntilEnd(self, item): - dispatcher.send("see time until end") - - def onZoomAll(self, item): - dispatcher.send("show all") - - def onPlayPause(self, item): - # since the X coord in a curveview affects the handling, one - # of them may be able to pick this up - results = dispatcher.send("onPlayPause") - times = [t for listener, t in results if t is not None] - self.music.playOrPause(t=times[0] if times else None) - - def onSave(self, *args): - # only doing curves still. I hope to eliminate all this. - log.info("saving curves") - self.curveset.save() - log.info("saved") - - def makeStatusLines(self, master): - """various labels that listen for dispatcher signals""" - for row, (signame, textfilter) in enumerate([ - ('input time', lambda t: "%.2fs" % t), - ('output levels', lambda levels: textwrap.fill( - "; ".join([ - "%s:%.2f" % (n, v) for n, v in list(levels.items())[:2] if v - > 0 - ]), 70)), - ('update period', lambda t: "%.1fms" % (t * 1000)), - ('update status', lambda x: str(x)), - ]): - key = Gtk.Label("%s:" % signame) - value = Gtk.Label("") - master.resize(row + 1, 2) - master.attach(key, 0, 1, row, row + 1) - master.attach(value, 1, 2, row, row + 1) - key.set_alignment(1, 0) - value.set_alignment(0, 0) - - dispatcher.connect(lambda val, value=value, tf=textfilter: value. - set_text(tf(val)), - signame, - weak=False) - dispatcher.connect(lambda val: setattr(self, 'lastSeenInputTime', val), - 'input time', - weak=False) - master.show_all() - - def refreshCurveView(self): - wtree = self.wtree - mtimes = [ - os.path.getmtime(f) for f in [ - 'light9/curvecalc/curveview.py', - 'light9/curvecalc/zoomcontrol.py', - ] - ] - - if (not hasattr(self, 'curvesetView') or - self.curvesetView._mtimes != mtimes): - print("reload curveview.py") - curvesVBox = wtree.get_object("curves") - zoomControlBox = wtree.get_object("zoomControlBox") - [curvesVBox.remove(c) for c in curvesVBox.get_children()] - [zoomControlBox.remove(c) for c in zoomControlBox.get_children()] - try: - linecache.clearcache() - imp.reload(curveview) - - # old ones are not getting deleted right - if hasattr(self, 'curvesetView'): - self.curvesetView.live = False - - # mem problem somewhere; need to hold a ref to this - self.curvesetView = curveview.Curvesetview( - self.graph, curvesVBox, zoomControlBox, self.curveset) - self.curvesetView._mtimes = mtimes - - # this is scheduled after some tk shuffling, to - # try to minimize the number of times we redraw - # the curve at startup. If tk is very slow, it's - # ok. You'll just get some wasted redraws. - self.curvesetView.goLive() - except Exception: - print("reload failed:") - traceback.print_exc() - if self.opts.reload: - reactor.callLater(1, self.refreshCurveView) - - -class MaxTime(object): - """ - looks up the time in seconds for the session's current song - """ - - def __init__(self, graph, session): - self.graph, self.session = graph, session - graph.addHandler(self.update) - - def update(self): - song = self.graph.value(self.session, L9['currentSong']) - if song is None: - self.maxtime = 0 - return - musicfilename = showconfig.songOnDisk(song) - self.maxtime = wavelength(musicfilename) - log.info("new max time %r", self.maxtime) - dispatcher.send("max time", maxtime=self.maxtime) - - def get(self): - return self.maxtime - - -def launch(args, graph, session, opts, startTime, music): - - try: - song = URIRef(args[0]) - graph.patchObject(context=session, - subject=session, - predicate=L9['currentSong'], - newObject=song) - except IndexError: - pass - - curveset = Curveset(graph=graph, session=session) - - log.debug("startup: output %s", time.time() - startTime) - - mt = MaxTime(graph, session) - dispatcher.connect(lambda: mt.get(), "get max time", weak=False) - - start = Main(graph, opts, session, curveset, music) - out = Output(graph, session, music, curveset, start.currentSubterms) - - dispatcher.send("show all") - - if opts.startup_only: - log.debug("quitting now because of --startup-only") - return - - def hoverTimeResponse(requestHandler): - results = dispatcher.send("onPlayPause") - times = [t for listener, t in results if t is not None] - if not times: - requestHandler.set_status(404) - requestHandler.write("not hovering over any time") - return - with graph.currentState(tripleFilter=(session, L9['currentSong'], - None)) as g: - song = g.value(session, L9['currentSong']) - json.dump({"song": song, "hoverTime": times[0]}, requestHandler) - - serveCurveEdit(networking.curveCalc.port, hoverTimeResponse, start.curveset) - - -def main(): - startTime = time.time() - parser = optparse.OptionParser() - parser.set_usage("%prog [opts] [songURI]") - parser.add_option("--debug", action="store_true", help="log at DEBUG") - parser.add_option("--reload", - action="store_true", - help="live reload of themes and code") - parser.add_option("--startup-only", - action='store_true', - help="quit after loading everything (for timing tests)") - parser.add_option("--profile", help='"hotshot" or "stat"') - clientsession.add_option(parser) - opts, args = parser.parse_args() - - log.setLevel(logging.DEBUG if opts.debug else logging.INFO) - - log.debug("startup: music %s", time.time() - startTime) - - session = clientsession.getUri('curvecalc', opts) - - music = Music() - graph = SyncedGraph(networking.rdfdb.url, "curvecalc") - - graph.initiallySynced.addCallback(lambda _: launch(args, graph, session, - opts, startTime, music)) - from light9 import prof - prof.run(reactor.run, profile=opts.profile) - - -main() diff -r 623836db99af -r 4556eebe5d73 bin/curvecalc_all_subterms --- a/bin/curvecalc_all_subterms Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,6 +0,0 @@ -#!/bin/zsh -echo broken: use a plain shell loop -exit 1 - - -for x (`ls $LIGHT9_SHOW/subterms`) { bin/curvecalc $x } diff -r 623836db99af -r 4556eebe5d73 bin/debug/clientdemo --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/debug/clientdemo Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,39 @@ +#!bin/python + +import os, sys +sys.path.append(".") +from twisted.internet import reactor +import cyclone.web, cyclone.httpclient, logging +from rdflib import Namespace, Literal, URIRef +from light9 import networking +from rdfdb.patch import Patch +from rdfdb.syncedgraph import SyncedGraph + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + log = logging.getLogger() + + g = SyncedGraph(networking.rdfdb.url, "clientdemo") + + from light9.Submaster import PersistentSubmaster + sub = PersistentSubmaster( + graph=g, uri=URIRef("http://light9.bigasterisk.com/sub/bcools")) + + #get sub to show its updating name, then push that all the way into KC gui so we can see just names refresh in there + + L9 = Namespace("http://light9.bigasterisk.com/") + + def updateDemoValue(): + v = list(g.objects(L9['demo'], L9['is'])) + print("demo value is %r" % v) + + g.addHandler(updateDemoValue) + + def adj(): + g.patch( + Patch(addQuads=[(L9['demo'], L9['is'], Literal(os.getpid()), + L9['clientdemo'])], + delQuads=[])) + + reactor.callLater(2, adj) + reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/debug/collector_loadtest.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/debug/collector_loadtest.py Sun May 12 19:02:10 2024 -0700 @@ -0,0 +1,52 @@ +#!bin/python +import asyncio +import logging +import random +from rdflib import URIRef +import time + +from light9.collector.collector_client_asyncio import sendToCollector +from light9.effect.settings import DeviceSettings +from light9.namespaces import DEV, L9 +from light9.run_local import log + +log.setLevel(logging.DEBUG) + + +async def loadTest(): + print("scheduling loadtest") + n = 200000 + period=.02 + times = [] + session = "loadtest%s" % time.time() + for i in range(n): + if i % 100 == 0: + log.info('sendToCollector %s', i) + start = time.time() + await sendToCollector( + "http://localhost:8202/", + session, + DeviceSettings( + graph=None, + settingsList=[ + # [DEV["backlight1"], L9["color"], "#ffffff"], # + # [DEV["backlight2"], L9["color"], "#ffffff"], + # [DEV["backlight3"], L9["color"], "#ffffff"], + # [DEV["backlight4"], L9["color"], "#ffffff"], + # [DEV["backlight5"], L9["color"], "#ffffff"], + # [DEV["down2"], L9["color"], "#ffffff"], + # [DEV["down3"], L9["color"], "#ffffff"], + # [DEV["down4"], L9["color"], "#ffffff"], + [URIRef('http://light9.bigasterisk.com/theater/skyline/device/down1'), L9["brightness"], random.random()], + [DEV["backlight5"], L9["uv"], 0.011] + ])) + times.append(time.time() - start) + await asyncio.sleep(period) + + print("loadtest done") + with open('/tmp/times', 'w') as f: + f.write(''.join('%s\n' % t for t in times)) + + +if __name__ == '__main__': + asyncio.run(loadTest()) diff -r 623836db99af -r 4556eebe5d73 bin/dmx_color_test.py --- a/bin/dmx_color_test.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ -#!bin/python -from run_local import log -import colorsys, time, logging -from light9 import dmxclient -from twisted.internet import reactor, task - -log.setLevel(logging.INFO) -firstDmxChannel = 10 - - -def step(): - hue = (time.time() * .2) % 1.0 - r, g, b = colorsys.hsv_to_rgb(hue, 1, 1) - chans = [r, g, b] - log.info(chans) - dmxclient.outputlevels([0] * (firstDmxChannel - 1) + chans, twisted=True) - - -task.LoopingCall(step).start(.05) -reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/dmxserver --- a/bin/dmxserver Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,280 +0,0 @@ -#!bin/python -""" -Replaced by bin/collector - - -this is the only process to talk to the dmx hardware. other clients -can connect to this server and present dmx output, and this server -will max ('pile-on') all the client requests. - -this server has a level display which is the final set of values that -goes to the hardware. - -clients shall connect to the xmlrpc server and send: - - their PID (or some other cookie) - - a length-n list of 0..1 levels which will represent the channel - values for the n first dmx channels. - -server is port 8030; xmlrpc method is called outputlevels(pid,levellist). - -todo: - save dmx on quit and restore on restart - if parport fails, run in dummy mode (and make an option for that too) -""" - -from twisted.internet import reactor -from twisted.web import xmlrpc, server -import sys, time, os -from optparse import OptionParser -import run_local -import txosc.dispatch, txosc. async -from light9.io import ParportDMX, UsbDMX - -from light9.updatefreq import Updatefreq -from light9 import networking - -from txzmq import ZmqEndpoint, ZmqFactory, ZmqPullConnection, ZmqRequestTimeoutError -import json - - -def startZmq(port, outputlevels): - zf = ZmqFactory() - e = ZmqEndpoint('bind', 'tcp://*:%s' % port) - s = ZmqPullConnection(zf, e) - - def onPull(message): - msg = json.loads(message[0]) - outputlevels(msg['clientid'], msg['levellist']) - - s.onPull = onPull - - -class ReceiverApplication(object): - """ - receive UDP OSC messages. address is /dmx/1 for dmx channel 1, - arguments are 0-1 floats for that channel and any number of - following channels. - """ - - def __init__(self, port, lightServer): - self.port = port - self.lightServer = lightServer - self.receiver = txosc.dispatch.Receiver() - self.receiver.addCallback("/dmx/*", self.pixel_handler) - self._server_port = reactor.listenUDP( - self.port, - txosc. async .DatagramServerProtocol(self.receiver), - interface='0.0.0.0') - print("Listening OSC on udp port %s" % (self.port)) - - def pixel_handler(self, message, address): - # this is already 1-based though I don't know why - startChannel = int(message.address.split('/')[2]) - levels = [a.value for a in message.arguments] - allLevels = [0] * (startChannel - 1) + levels - self.lightServer.xmlrpc_outputlevels("osc@%s" % startChannel, allLevels) - - -class XMLRPCServe(xmlrpc.XMLRPC): - - def __init__(self, options): - - xmlrpc.XMLRPC.__init__(self) - - self.clientlevels = {} # clientID : list of levels - self.lastseen = {} # clientID : time last seen - self.clientfreq = {} # clientID : updatefreq - - self.combinedlevels = [] # list of levels, after max'ing the clients - self.clientschanged = 1 # have clients sent anything since the last send? - self.options = options - self.lastupdate = 0 # time of last dmx send - self.laststatsprint = 0 # time - - # desired seconds between sendlevels() calls - self.calldelay = 1 / options.updates_per_sec - - print("starting parport connection") - self.parportdmx = UsbDMX(dimmers=90, port=options.dmx_device) - if os.environ.get('DMXDUMMY', 0): - self.parportdmx.godummy() - else: - self.parportdmx.golive() - - self.updatefreq = Updatefreq() # freq of actual dmx sends - self.num_unshown_updates = None - self.lastshownlevels = None - # start the loop - self.sendlevels() - - # the other loop - self.purgeclients() - - def purgeclients(self): - """forget about any clients who haven't sent levels in a while. - this runs in a loop""" - - purge_age = 10 # seconds - - reactor.callLater(1, self.purgeclients) - - now = time.time() - cids = list(self.lastseen.keys()) - for cid in cids: - lastseen = self.lastseen[cid] - if lastseen < now - purge_age: - print(("forgetting client %s (no activity for %s sec)" % - (cid, purge_age))) - try: - del self.clientlevels[cid] - except KeyError: - pass - del self.clientfreq[cid] - del self.lastseen[cid] - - def sendlevels(self): - """sends to dmx if levels have changed, or if we havent sent - in a while""" - - reactor.callLater(self.calldelay, self.sendlevels) - - if self.clientschanged: - # recalc levels - - self.calclevels() - - if (self.num_unshown_updates is None or # first time - self.options.fast_updates or # show always - ( - self.combinedlevels != self.lastshownlevels and # changed - self.num_unshown_updates > 5)): # not too frequent - self.num_unshown_updates = 0 - self.printlevels() - self.lastshownlevels = self.combinedlevels[:] - else: - self.num_unshown_updates += 1 - - if time.time() > self.laststatsprint + 2: - self.laststatsprint = time.time() - self.printstats() - - # used to be a fixed 1 in here, for the max delay between - # calls, instead of calldelay - if self.clientschanged or time.time( - ) > self.lastupdate + self.calldelay: - self.lastupdate = time.time() - self.sendlevels_dmx() - - self.clientschanged = 0 # clear the flag - - def calclevels(self): - """combine all the known client levels into self.combinedlevels""" - self.combinedlevels = [] - for chan in range(0, self.parportdmx.dimmers): - x = 0 - for clientlist in list(self.clientlevels.values()): - if len(clientlist) > chan: - # clamp client levels to 0..1 - cl = max(0, min(1, clientlist[chan])) - x = max(x, cl) - self.combinedlevels.append(x) - - def printlevels(self): - """write all the levels to stdout""" - print("Levels:", - "".join(["% 2d " % (x * 100) for x in self.combinedlevels])) - - def printstats(self): - """print the clock, freq, etc, with a \r at the end""" - - sys.stdout.write("dmxserver up at %s, [polls %s] " % ( - time.strftime("%H:%M:%S"), - str(self.updatefreq), - )) - for cid, freq in list(self.clientfreq.items()): - sys.stdout.write("[%s %s] " % (cid, str(freq))) - sys.stdout.write("\r") - sys.stdout.flush() - - def sendlevels_dmx(self): - """output self.combinedlevels to dmx, and keep the updates/sec stats""" - # they'll get divided by 100 - if self.parportdmx: - self.parportdmx.sendlevels([l * 100 for l in self.combinedlevels]) - self.updatefreq.update() - - def xmlrpc_echo(self, x): - return x - - def xmlrpc_outputlevels(self, cid, levellist): - """send a unique id for your client (name+pid maybe), then - the variable-length dmx levellist (scaled 0..1)""" - if levellist != self.clientlevels.get(cid, None): - self.clientlevels[cid] = levellist - self.clientschanged = 1 - self.trackClientFreq(cid) - return "ok" - - def xmlrpc_currentlevels(self, cid): - """get a list of levels we're currently sending out. All - channels beyond the list you get back, they're at zero.""" - # if this is still too slow, it might help to return a single - # pickled string - self.trackClientFreq(cid) - trunc = self.combinedlevels[:] - i = len(trunc) - 1 - if i < 0: - return [] - while trunc[i] == 0 and i >= 0: - i -= 1 - if i < 0: - return [] - trunc = trunc[:i + 1] - return trunc - - def trackClientFreq(self, cid): - if cid not in self.lastseen: - print("hello new client %s" % cid) - self.clientfreq[cid] = Updatefreq() - self.lastseen[cid] = time.time() - self.clientfreq[cid].update() - - -parser = OptionParser() -parser.add_option("-f", - "--fast-updates", - action='store_true', - help=('display all dmx output to stdout instead ' - 'of the usual reduced output')) -parser.add_option("-r", - "--updates-per-sec", - type='float', - default=20, - help=('dmx output frequency')) -parser.add_option("-d", - "--dmx-device", - default='/dev/dmx0', - help='dmx device name') -parser.add_option("-n", - "--dummy", - action="store_true", - help="dummy mode, same as DMXDUMMY=1 env variable") -(options, songfiles) = parser.parse_args() - -print(options) - -if options.dummy: - os.environ['DMXDUMMY'] = "1" - -port = networking.dmxServer.port -print("starting xmlrpc server on port %s" % port) -xmlrpcServe = XMLRPCServe(options) -reactor.listenTCP(port, server.Site(xmlrpcServe)) - -startZmq(networking.dmxServerZmq.port, xmlrpcServe.xmlrpc_outputlevels) - -oscApp = ReceiverApplication(9051, xmlrpcServe) - -reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/effectListing --- a/bin/effectListing Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ -#!/bin/sh -pnpm exec vite -c light9/effect/listing/web/vite.config.ts & -wait - diff -r 623836db99af -r 4556eebe5d73 bin/effectSequencer --- a/bin/effectSequencer Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,5 +0,0 @@ -#!/bin/sh -pnpm exec vite -c light9/effect/sequencer/web/vite.config.ts & -pdm run uvicorn light9.effect.sequencer.service:app --host 0.0.0.0 --port 8213 --no-access-log -wait - diff -r 623836db99af -r 4556eebe5d73 bin/effecteval --- a/bin/effecteval Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,283 +0,0 @@ -#!bin/python - -from run_local import log -from twisted.internet import reactor -from twisted.internet.defer import inlineCallbacks, returnValue -import cyclone.web, cyclone.websocket, cyclone.httpclient -import sys, optparse, logging, json, itertools -from rdflib import URIRef, Literal - -from light9 import networking, showconfig -from light9.effecteval.effect import EffectNode -from light9.effect.edit import getMusicStatus, songNotePatch -from light9.effecteval.effectloop import makeEffectLoop -from light9.metrics import metrics, metricsRoute -from light9.namespaces import L9 -from rdfdb.patch import Patch -from rdfdb.syncedgraph import SyncedGraph - -from cycloneerr import PrettyErrorHandler -from light9.coffee import StaticCoffee - - - -class EffectEdit(PrettyErrorHandler, cyclone.web.RequestHandler): - - def get(self): - self.set_header('Content-Type', 'text/html') - self.write(open("light9/effecteval/effect.html").read()) - - def delete(self): - graph = self.settings.graph - uri = URIRef(self.get_argument('uri')) - with graph.currentState(tripleFilter=(None, L9['effect'], uri)) as g: - song = ctx = list(g.subjects(L9['effect'], uri))[0] - self.settings.graph.patch( - Patch(delQuads=[ - (song, L9['effect'], uri, ctx), - ])) - - -@inlineCallbacks -def currentSong(): - s = (yield getMusicStatus())['song'] - if s is None: - raise ValueError("no current song") - returnValue(URIRef(s)) - - -class SongEffects(PrettyErrorHandler, cyclone.web.RequestHandler): - - def wideOpenCors(self): - self.set_header('Access-Control-Allow-Origin', '*') - self.set_header('Access-Control-Allow-Methods', - 'GET, PUT, POST, DELETE, OPTIONS') - self.set_header('Access-Control-Max-Age', '1000') - self.set_header('Access-Control-Allow-Headers', - 'Content-Type, Authorization, X-Requested-With') - - def options(self): - self.wideOpenCors() - self.write('') - - @inlineCallbacks - def post(self): - self.wideOpenCors() - dropped = URIRef(self.get_argument('drop')) - - try: - song = URIRef(self.get_argument('uri')) - except Exception: # which? - song = yield currentSong() - - event = self.get_argument('event', default='default') - - note = self.get_argument('note', default=None) - if note is not None: - note = URIRef(note) - - log.info("adding to %s", song) - note, p = yield songNotePatch(self.settings.graph, - dropped, - song, - event, - ctx=song, - note=note) - - self.settings.graph.patch(p) - self.settings.graph.suggestPrefixes(song, {'song': URIRef(song + '/')}) - self.write(json.dumps({'note': note})) - - -class SongEffectsUpdates(cyclone.websocket.WebSocketHandler): - - def connectionMade(self, *args, **kwargs): - self.graph = self.settings.graph - self.graph.addHandler(self.updateClient) - - def updateClient(self): - # todo: abort if client is gone - playlist = self.graph.value(showconfig.showUri(), L9['playList']) - songs = list(self.graph.items(playlist)) - out = [] - for s in songs: - out.append({'uri': s, 'label': self.graph.label(s), 'effects': []}) - seen = set() - for n in self.graph.objects(s, L9['note']): - for uri in self.graph.objects(n, L9['effectClass']): - if uri in seen: - continue - seen.add(uri) - out[-1]['effects'].append({ - 'uri': uri, - 'label': self.graph.label(uri) - }) - out[-1]['effects'].sort(key=lambda e: e['uri']) - - self.sendMessage({'songs': out}) - - -class EffectUpdates(cyclone.websocket.WebSocketHandler): - """ - stays alive for the life of the effect page - """ - - def connectionMade(self, *args, **kwargs): - log.info("websocket opened") - self.uri = URIRef(self.get_argument('uri')) - self.sendMessage({'hello': repr(self)}) - - self.graph = self.settings.graph - self.graph.addHandler(self.updateClient) - - def updateClient(self): - # todo: if client has dropped, abort and don't get any more - # graph updates - - # EffectNode knows how to put them in order. Somehow this is - # not triggering an update when the order changes. - en = EffectNode(self.graph, self.uri) - codeLines = [c.code for c in en.codes] - self.sendMessage({'codeLines': codeLines}) - - def connectionLost(self, reason): - log.info("websocket closed") - - def messageReceived(self, message): - 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')) - 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 received on PUT /code") - return - with self.settings.graph.currentState(tripleFilter=(None, L9['effect'], - effect)) as g: - song = next(g.subjects(L9['effect'], effect)) - - 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) - - -class EffectEval(PrettyErrorHandler, cyclone.web.RequestHandler): - - @inlineCallbacks - def get(self): - # return dmx list for that effect - uri = URIRef(self.get_argument('uri')) - response = yield cyclone.httpclient.fetch( - networking.musicPlayer.path('time')) - songTime = json.loads(response.body)['t'] - - node = EffectNode(self.settings.graph, uri) - outSub = node.eval(songTime) - self.write(json.dumps(outSub.get_dmx_list())) - - -# Completely not sure where the effect background loop should -# go. Another process could own it, and get this request repeatedly: -class SongEffectsEval(PrettyErrorHandler, cyclone.web.RequestHandler): - - def get(self): - song = URIRef(self.get_argument('song')) - effects = effectsForSong(self.settings.graph, song) # noqa - raise NotImplementedError - self.write(maxDict(effectDmxDict(e) for e in effects)) # noqa - # return dmx dict for all effects in the song, already combined - - -class App(object): - - def __init__(self, show, outputWhere): - self.show = show - self.outputWhere = outputWhere - self.graph = SyncedGraph(networking.rdfdb.url, "effectEval") - self.graph.initiallySynced.addCallback(self.launch).addErrback( - log.error) - - def launch(self, *args): - log.info('launch') - if self.outputWhere: - self.loop = makeEffectLoop(self.graph, self.outputWhere) - self.loop.startLoop() - - SFH = cyclone.web.StaticFileHandler - self.cycloneApp = cyclone.web.Application(handlers=[ - (r'/()', SFH, { - 'path': 'light9/effecteval', - 'default_filename': 'index.html' - }), - (r'/effect', EffectEdit), - (r'/effect\.js', StaticCoffee, { - 'src': 'light9/effecteval/effect.coffee' - }), - (r'/(effect-components\.html)', SFH, { - 'path': 'light9/effecteval' - }), - (r'/effectUpdates', EffectUpdates), - (r'/code', Code), - (r'/songEffectsUpdates', SongEffectsUpdates), - (r'/effect/eval', EffectEval), - (r'/songEffects', SongEffects), - (r'/songEffects/eval', SongEffectsEval), - metricsRoute(), - ], - debug=True, - graph=self.graph) - reactor.listenTCP(networking.effectEval.port, self.cycloneApp) - log.info("listening on %s" % networking.effectEval.port) - - -if __name__ == "__main__": - parser = optparse.OptionParser() - parser.add_option( - '--show', - help='show URI, like http://light9.bigasterisk.com/show/dance2008', - default=showconfig.showUri()) - parser.add_option("-v", - "--verbose", - action="store_true", - help="logging.DEBUG") - parser.add_option("--twistedlog", - action="store_true", - help="twisted logging") - parser.add_option("--output", metavar="WHERE", help="dmx or leds") - (options, args) = parser.parse_args() - log.setLevel(logging.DEBUG if options.verbose else logging.INFO) - - if not options.show: - raise ValueError("missing --show http://...") - - app = App(URIRef(options.show), options.output) - if options.twistedlog: - from twisted.python import log as twlog - twlog.startLogging(sys.stderr) - reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/fade --- a/bin/fade Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ -#!/bin/sh -exec pnpm exec vite -c light9/fade/vite.config.ts - - diff -r 623836db99af -r 4556eebe5d73 bin/gobutton --- a/bin/gobutton Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,3 +0,0 @@ -#!/bin/sh -# uri should be set from $LIGHT9_SHOW/config.n3 -exec curl --silent -d '' http://localhost:8040/go diff -r 623836db99af -r 4556eebe5d73 bin/gtk_dnd_demo.py --- a/bin/gtk_dnd_demo.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ -#!bin/python -import run_local -import gtk -import sys -sys.path.append(".") -from rdflib import URIRef -from light9 import networking -from light9.editchoicegtk import EditChoice, Local -from light9.observable import Observable -from rdfdb.syncedgraph import SyncedGraph - -win = gtk.Window() - -graph = SyncedGraph(networking.rdfdb.url, "gtkdnddemo") - -r1 = URIRef("http://example.com/interestingThing") -v = Observable(r1) -win.add(EditChoice(graph, v)) -win.show_all() -gtk.main() diff -r 623836db99af -r 4556eebe5d73 bin/inputdemo --- a/bin/inputdemo Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,65 +0,0 @@ -#!bin/python -import sys -sys.path.append('/usr/lib/python2.7/dist-packages') # For gtk -from twisted.internet import gtk3reactor -gtk3reactor.install() -from twisted.internet import reactor -from rdflib import URIRef -import optparse, logging, time -from gi.repository import Gtk -from run_local import log -from light9 import networking -from light9 import clientsession -from rdfdb.syncedgraph import SyncedGraph -from light9.curvecalc.client import sendLiveInputPoint - - -class App(object): - - def __init__(self): - parser = optparse.OptionParser() - parser.set_usage("%prog [opts] [curve uri]") - parser.add_option("--debug", action="store_true", help="log at DEBUG") - clientsession.add_option(parser) - opts, args = parser.parse_args() - - log.setLevel(logging.DEBUG if opts.debug else logging.INFO) - - self.session = clientsession.getUri('inputdemo', opts) - self.graph = SyncedGraph(networking.rdfdb.url, "inputdemo") - - self.graph.initiallySynced.addCallback(lambda _: self.launch()) - - self.curve = args[0] if args else URIRef( - 'http://light9.bigasterisk.com/show/dance2014/song1/curve/c-1401259747.675542' - ) - print("sending points on curve %s" % self.curve) - - reactor.run() - - def launch(self): - win = Gtk.Window() - - slider = Gtk.Scale.new_with_range(orientation=Gtk.Orientation.VERTICAL, - min=0, - max=1, - step=.001) - slider.props.inverted = True - slider.connect('value-changed', self.onChanged) - - win.add(slider) - win.parse_geometry('50x250') - win.connect("delete-event", lambda *a: reactor.crash()) - win.connect("destroy", lambda *a: reactor.crash()) - win.show_all() - - def onChanged(self, scale): - t1 = time.time() - d = sendLiveInputPoint(self.curve, scale.get_value()) - - @d.addCallback - def done(result): - print("posted in %.1f ms" % (1000 * (time.time() - t1))) - - -App() diff -r 623836db99af -r 4556eebe5d73 bin/inputquneo --- a/bin/inputquneo Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,115 +0,0 @@ -#!bin/python -""" -read Quneo midi events, write to curvecalc and maybe to effects -""" - -from run_local import log -import logging, urllib.request, urllib.parse, urllib.error -import cyclone.web, cyclone.httpclient -from rdflib import URIRef -from twisted.internet import reactor, task -from light9.curvecalc.client import sendLiveInputPoint -from light9.namespaces import L9, RDF -from rdfdb.syncedgraph import SyncedGraph -from light9 import networking - -import sys -sys.path.append('/usr/lib/python2.7/dist-packages') # For pygame -import pygame.midi - -curves = { - 23: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-2'), - 24: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-3'), - 25: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-4'), - 6: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-5'), - 18: URIRef('http://light9.bigasterisk.com/show/dance2014/song1/curve/c-6'), -} - - -class WatchMidi: - - def __init__(self, graph): - self.graph = graph - pygame.midi.init() - - dev = self.findQuneo() - self.inp = pygame.midi.Input(dev) - task.LoopingCall(self.step).start(.05) - - self.noteIsOn = {} - - self.effectMap = {} # note: effect class uri - self.graph.addHandler(self.setupNotes) - - def setupNotes(self): - for e in self.graph.subjects(RDF.type, L9['EffectClass']): - qn = self.graph.value(e, L9['quneoNote']) - if qn: - self.effectMap[int(qn)] = e - log.info("setup with %s effects", len(self.effectMap)) - - def findQuneo(self): - for dev in range(pygame.midi.get_count()): - interf, name, isInput, isOutput, opened = pygame.midi.get_device_info( - dev) - if 'QUNEO' in name and isInput: - return dev - raise ValueError("didn't find quneo input device") - - def step(self): - if not self.inp.poll(): - return - NOTEON, NOTEOFF = 144, 128 - for ev in self.inp.read(999): - (status, d1, d2, _), _ = ev - if status in [NOTEON, NOTEOFF]: - print(status, d1, d2) - - if status == NOTEON: - if not self.noteIsOn.get(d1): - self.noteIsOn[d1] = True - try: - e = self.effectMap[d1] - cyclone.httpclient.fetch( - url=networking.effectEval.path('songEffects'), - method='POST', - headers={ - 'Content-Type': - ['application/x-www-form-urlencoded'] - }, - postdata=urllib.parse.urlencode([('drop', e)]), - ) - except KeyError: - pass - - if status == NOTEOFF: - self.noteIsOn[d1] = False - - if 0: - # curve editing mode, not done yet - for group in [(23, 24, 25), (6, 18)]: - if d1 in group: - if not self.noteIsOn.get(group): - print("start zero") - - for d in group: - sendLiveInputPoint(curves[d], 0) - self.noteIsOn[group] = True - else: # miss first update - sendLiveInputPoint(curves[d1], d2 / 127) - - if status == 128: #noteoff - for d in group: - sendLiveInputPoint(curves[d], 0) - self.noteIsOn[group] = False - - -def main(): - log.setLevel(logging.DEBUG) - graph = SyncedGraph(networking.rdfdb.url, "inputQuneo") - wm = WatchMidi(graph) - reactor.run() - del wm - - -main() diff -r 623836db99af -r 4556eebe5d73 bin/kcclient --- a/bin/kcclient Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,16 +0,0 @@ -#!/usr/bin/env python -"""send KeyboardComposer a fade request, for use from the shell""" - -import sys -import run_local -from restclient import Resource -from light9 import networking - -subname = sys.argv[1] -level = sys.argv[2] -fadesecs = '0' -if len(sys.argv) > 3: - fadesecs = sys.argv[3] - -levelServer = Resource(networking.keyboardComposer.url) -levelServer.post('fadesub', subname=subname, level=level, secs=fadesecs) diff -r 623836db99af -r 4556eebe5d73 bin/keyboardcomposer --- a/bin/keyboardcomposer Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,712 +0,0 @@ -#!bin/python - -from run_local import log - -from optparse import OptionParser -from typing import Any, Dict, Tuple, List -import cgi, time, logging -import imp -import tkinter.tix as tk - -from louie import dispatcher -from rdflib import URIRef, Literal -from twisted.internet import reactor, tksupport -from twisted.web import resource -import webcolors, colorsys - -from bcf2000 import BCF2000 -from light9 import clientsession -from light9 import showconfig, networking, prof -from light9.Fadable import Fadable -from light9.effect.sequencer import CodeWatcher -from light9.effect.settings import DeviceSettings -from light9.effect.simple_outputs import SimpleOutputs -from light9.namespaces import L9, RDF, RDFS -from light9.subclient import SubClient -from light9.tkdnd import initTkdnd, dragSourceRegister, dropTargetRegister -from light9.uihelpers import toplevelat -from rdfdb.patch import Patch -from rdfdb.syncedgraph import SyncedGraph -import light9.effect.effecteval - -nudge_keys = {'up': list('qwertyui'), 'down': list('asdfghjk')} - - -class DummySliders: - - def valueOut(self, name, value): - pass - - def close(self): - pass - - def reopen(self): - pass - - -class SubScale(tk.Scale, Fadable): - - def __init__(self, master, *args, **kw): - self.scale_var = kw.get('variable') or tk.DoubleVar() - kw.update({ - 'variable': self.scale_var, - 'from': 1., - 'to': 0., - 'showvalue': 0, - 'sliderlength': 15, - 'res': 0.001, - 'width': 40, - 'troughcolor': 'black', - 'bg': 'grey40', - 'highlightthickness': 1, - 'bd': 1, - 'highlightcolor': 'red', - 'highlightbackground': 'black', - 'activebackground': 'red' - }) - tk.Scale.__init__(self, master, *args, **kw) - Fadable.__init__(self, var=self.scale_var, wheel_step=0.05) - self.draw_indicator_colors() - - def draw_indicator_colors(self): - if self.scale_var.get() == 0: - self['troughcolor'] = 'black' - else: - self['troughcolor'] = 'blue' - - -class SubmasterBox(tk.Frame): - """ - this object owns the level of the submaster (the rdf graph is the - real authority) - - This leaks handlers or DoubleVars or something and tries to just - skip the obsolete ones. It'll get slower and bigger over - time. todo: make aa web version. - """ - - def __init__(self, master, graph, sub, session, col, row): - self.graph = graph - self.sub = sub - self.session = session - self.col, self.row = col, row - bg = self.graph.value(sub, L9['color'], default='#000000') - rgb = webcolors.hex_to_rgb(bg) - hsv = colorsys.rgb_to_hsv(*[x / 255 for x in rgb]) - darkBg = webcolors.rgb_to_hex( - tuple([ - int(x * 255) for x in colorsys.hsv_to_rgb(hsv[0], hsv[1], .2) - ])) - tk.Frame.__init__(self, master, bd=1, relief='raised', bg=bg) - self.name = self.graph.label(sub) - self._val = 0.0 - self.slider_var = tk.DoubleVar() - self.pauseTrace = False - self.scale = SubScale(self, variable=self.slider_var, width=20) - self.dead = False - - self.namelabel = tk.Label(self, - font="Arial 9", - bg=darkBg, - fg='white', - pady=0) - self.graph.addHandler(self.updateName) - - self.namelabel.pack(side=tk.TOP) - self.levellabel = tk.Label(self, - textvariable=self.slider_var, - font="Arial 6", - bg='black', - fg='white', - pady=0) - self.levellabel.pack(side=tk.TOP) - self.scale.pack(side=tk.BOTTOM, expand=1, fill=tk.BOTH) - - for w in [self, self.namelabel, self.levellabel]: - dragSourceRegister(w, 'copy', 'text/uri-list', sub) - - self._slider_var_trace = self.slider_var.trace('w', self.slider_changed) - - self.graph.addHandler(self.updateLevelFromGraph) - - # initial position - # stil need? dispatcher.send("send_to_hw", sub=sub.uri, hwCol=col + 1) - - def getVal(self) -> float: - return self._val - - def setVal(self, newVal: float) -> None: - if self.dead: - return - try: - self.scale.set(newVal) - self.levellabel.config(text=str(newVal)) - except Exception: - log.warning("disabling handlers on broken subbox") - self.dead = True - - def cleanup(self): - self.slider_var.trace_vdelete('w', self._slider_var_trace) - - def slider_changed(self, *args): - self._val = self.scale.get() - self.scale.draw_indicator_colors() - - if self.pauseTrace: - return - self.updateGraphWithLevel(self.sub, self.getVal()) - - # needs fixing: plan is to use dispatcher or a method call to tell a hardware-mapping object who changed, and then it can make io if that's a current hw slider - dispatcher.send("send_to_hw", - sub=self.sub, - hwCol=self.col + 1, - boxRow=self.row) - - def updateGraphWithLevel(self, uri, level): - """in our per-session graph, we maintain SubSetting objects like this: - - ?session :subSetting [a :SubSetting; :sub ?s; :level ?l] - """ - # move to syncedgraph patchMapping - - self.graph.patchMapping(context=self.session, - subject=self.session, - predicate=L9['subSetting'], - nodeClass=L9['SubSetting'], - keyPred=L9['sub'], - newKey=uri, - valuePred=L9['level'], - newValue=Literal(level)) - - def updateLevelFromGraph(self): - """read rdf level, write it to subbox.slider_var""" - # move this to syncedgraph readMapping - graph = self.graph - - for setting in graph.objects(self.session, L9['subSetting']): - if graph.value(setting, L9['sub']) == self.sub: - self.pauseTrace = True # don't bounce this update back to server - try: - self.setVal(graph.value(setting, L9['level']).toPython()) - finally: - self.pauseTrace = False - - def updateName(self): - if self.scale is None: - return - - def shortUri(u): - return '.../' + u.split('/')[-1] - - try: - self.namelabel.config( - text=self.graph.label(self.sub) or shortUri(self.sub)) - except Exception: - log.warn("disabling handlers on broken subbox") - self.scale = None - - -class KeyboardComposer(tk.Frame, SubClient): - - def __init__(self, - root: tk.Tk, - graph: SyncedGraph, - session: URIRef, - hw_sliders=True): - tk.Frame.__init__(self, root, bg='black') - SubClient.__init__(self) - self.graph = graph - self.session = session - - self.subbox: Dict[URIRef, SubmasterBox] = {} # sub uri : SubmasterBox - self.slider_table: Dict[Tuple[int, int], SubmasterBox] = { - } # coords : SubmasterBox - self.rows: List[tk.Frame] = [] # this holds Tk Frames for each row - - self.current_row = 0 # should come from session graph - - self.use_hw_sliders = hw_sliders - self.connect_to_hw(hw_sliders) - - self.make_key_hints() - self.make_buttons() - - self.graph.addHandler(self.redraw_sliders) - - self.codeWatcher = CodeWatcher( - onChange=lambda: self.graph.addHandler(self.redraw_sliders)) - - self.send_levels_loop(periodSec=.05) - self.graph.addHandler(self.rowFromGraph) - - def make_buttons(self): - self.buttonframe = tk.Frame(self, bg='black') - self.buttonframe.pack(side=tk.BOTTOM) - - self.sliders_status_var = tk.IntVar() - self.sliders_status_var.set(self.use_hw_sliders) - self.sliders_checkbutton = tk.Checkbutton( - self.buttonframe, - text="Sliders", - variable=self.sliders_status_var, - command=lambda: self.toggle_slider_connectedness(), - bg='black', - fg='white') - self.sliders_checkbutton.pack(side=tk.LEFT) - - self.alltozerobutton = tk.Button(self.buttonframe, - text="All to Zero", - command=self.alltozero, - bg='black', - fg='white') - self.alltozerobutton.pack(side='left') - - self.save_stage_button = tk.Button( - self.buttonframe, - text="Save", - command=lambda: self.save_current_stage(self.sub_name.get()), - bg='black', - fg='white') - self.save_stage_button.pack(side=tk.LEFT) - self.sub_name = tk.Entry(self.buttonframe, bg='black', fg='white') - self.sub_name.pack(side=tk.LEFT) - - def redraw_sliders(self) -> None: - self.draw_sliders() - if len(self.rows): - self.change_row(self.current_row) - self.rows[self.current_row].focus() - - self.stop_frequent_update_time = 0 - - def draw_sliders(self): - for r in self.rows: - r.destroy() - self.rows = [] - for b in list(self.subbox.values()): - b.cleanup() - self.subbox.clear() - self.slider_table.clear() - - self.tk_focusFollowsMouse() - - rowcount = -1 - col = 0 - last_group = None - - withgroups = [] - for effect in self.graph.subjects(RDF.type, L9['Effect']): - withgroups.append((self.graph.value(effect, L9['group']), - self.graph.value(effect, L9['order']), - self.graph.label(effect), effect)) - withgroups.sort() - - log.debug("withgroups %s", withgroups) - - self.effectEval: Dict[URIRef, light9.effect.effecteval.EffectEval] = {} - imp.reload(light9.effect.effecteval) - simpleOutputs = SimpleOutputs(self.graph) - for group, order, sortLabel, effect in withgroups: - if col == 0 or group != last_group: - row = self.make_row(group) - rowcount += 1 - col = 0 - - subbox = SubmasterBox(row, self.graph, effect, self.session, col, - rowcount) - subbox.place(relx=col / 8, rely=0, relwidth=1 / 8, relheight=1) - self.subbox[effect] = self.slider_table[(rowcount, col)] = subbox - - self.setup_key_nudgers(subbox.scale) - - self.effectEval[effect] = light9.effect.effecteval.EffectEval( - self.graph, effect, simpleOutputs) - - col = (col + 1) % 8 - last_group = group - - def toggle_slider_connectedness(self): - self.use_hw_sliders = not self.use_hw_sliders - if self.use_hw_sliders: - self.sliders.reopen() - else: - self.sliders.close() - self.change_row(self.current_row) - self.rows[self.current_row].focus() - - def connect_to_hw(self, hw_sliders): - log.info('connect_to_hw') - if hw_sliders: - try: - self.sliders = Sliders(self) - log.info("connected to sliders") - except IOError as e: - log.info("no hardware sliders %r", e) - self.sliders = DummySliders() - self.use_hw_sliders = False - dispatcher.connect(self.send_to_hw, 'send_to_hw') - else: - self.sliders = DummySliders() - - def make_key_hints(self): - keyhintrow = tk.Frame(self) - - col = 0 - for upkey, downkey in zip(nudge_keys['up'], nudge_keys['down']): - # what a hack! - downkey = downkey.replace('semicolon', ';') - upkey, downkey = (upkey.upper(), downkey.upper()) - - # another what a hack! - keylabel = tk.Label(keyhintrow, - text='%s\n%s' % (upkey, downkey), - width=1, - font=('Arial', 10), - bg='red', - fg='white', - anchor='c') - keylabel.pack(side=tk.LEFT, expand=1, fill=tk.X) - col += 1 - - keyhintrow.pack(fill=tk.X, expand=0) - self.keyhints = keyhintrow - - def setup_key_nudgers(self, tkobject): - for d, keys in list(nudge_keys.items()): - for key in keys: - # lowercase makes full=0 - keysym = "" % key - tkobject.bind(keysym, - lambda evt, num=keys.index(key), d=d: self. - got_nudger(num, d)) - - # uppercase makes full=1 - keysym = "" % key.upper() - keysym = keysym.replace('SEMICOLON', 'colon') - tkobject.bind(keysym, - lambda evt, num=keys.index(key), d=d: self. - got_nudger(num, d, full=1)) - - # Row changing: - # Page dn, C-n, and ] do down - # Page up, C-p, and ' do up - for key in ' ' \ - ' '.split(): - tkobject.bind(key, self.change_row_cb) - - def change_row_cb(self, event): - diff = 1 - if event.keysym in ('Prior', 'p', 'bracketright'): - diff = -1 - self.change_row(self.current_row + diff) - - def rowFromGraph(self): - self.change_row(int( - self.graph.value(self.session, L9['currentRow'], default=0)), - fromGraph=True) - - def change_row(self, row: int, fromGraph=False) -> None: - old_row = self.current_row - self.current_row = row - self.current_row = max(0, self.current_row) - self.current_row = min(len(self.rows) - 1, self.current_row) - try: - row = self.rows[self.current_row] - except IndexError: - # if we're mid-load, this row might still appear soon. If - # we changed interactively, the user is out of bounds and - # needs to be brought back in - if fromGraph: - return - raise - - self.unhighlight_row(old_row) - self.highlight_row(self.current_row) - self.keyhints.pack_configure(before=row) - - if not fromGraph: - self.graph.patchObject(self.session, self.session, L9['currentRow'], - Literal(self.current_row)) - - for col in range(1, 9): - try: - subbox = self.slider_table[(self.current_row, col - 1)] - self.sliders.valueOut("button-upper%d" % col, True) - except KeyError: - # unfilled bottom row has holes (plus rows with incomplete - # groups - self.sliders.valueOut("button-upper%d" % col, False) - self.sliders.valueOut("slider%d" % col, 0) - continue - self.send_to_hw(sub=subbox.sub, hwCol=col, boxRow=self.current_row) - - def got_nudger(self, number, direction, full=0): - try: - subbox = self.slider_table[(self.current_row, number)] - except KeyError: - return - - if direction == 'up': - if full: - subbox.scale.fade(1) - else: - subbox.scale.increase() - else: - if full: - subbox.scale.fade(0) - else: - subbox.scale.decrease() - - def hw_slider_moved(self, col, value): - value = int(value * 100) / 100 - try: - subbox = self.slider_table[(self.current_row, col)] - except KeyError: - return # no slider assigned at that column - - if hasattr(self, 'pendingHwSet'): - import twisted.internet.error - try: - self.pendingHwSet.cancel() - except twisted.internet.error.AlreadyCalled: - pass - self.pendingHwSet = reactor.callLater(.01, subbox.setVal, value) - - def send_to_hw(self, sub, hwCol, boxRow): - if isinstance(self.sliders, DummySliders): - return - - assert isinstance(sub, URIRef), repr(sub) - - if boxRow != self.current_row: - return - - try: - level = self.get_levels()[sub] - except KeyError: - log.warn("%r not in %r", sub, self.get_levels()) - raise - v = round(127 * level) - chan = "slider%s" % hwCol - - # workaround for some rounding issue, where we receive one - # value and then decide to send back a value that's one step - # lower. -5 is a fallback for having no last value. hopefully - # we won't really see it - if abs(v - self.sliders.lastValue.get(chan, -5)) <= 1: - return - self.sliders.valueOut(chan, v) - - def make_row(self, group): - """group is a URI or None""" - row = tk.Frame(self, bd=2, bg='black') - row.subGroup = group - - def onDrop(ev): - self.change_group(sub=URIRef(ev.data), row=row) - return "link" - - dropTargetRegister(row, - onDrop=onDrop, - typeList=['*'], - hoverStyle=dict(background="#555500")) - - row.pack(expand=1, fill=tk.BOTH) - self.setup_key_nudgers(row) - self.rows.append(row) - return row - - def change_group(self, sub, row): - """update this sub's group, and maybe other sub groups as needed, so - this sub displays in this row""" - group = row.subGroup - self.graph.patchObject(context=self.session, - subject=sub, - predicate=L9['group'], - newObject=group) - - def highlight_row(self, row): - row = self.rows[row] - row['bg'] = 'red' - - def unhighlight_row(self, row): - row = self.rows[row] - row['bg'] = 'black' - - def get_levels(self): - return dict([ - (uri, box.getVal()) for uri, box in list(self.subbox.items()) - ]) - - def get_output_settings(self, _graph=None): - _graph = _graph or self.graph - outputSettings = [] - for setting in _graph.objects(self.session, L9['subSetting']): - effect = _graph.value(setting, L9['sub']) - strength = _graph.value(setting, L9['level']) - if strength: - now = time.time() - out, report = self.effectEval[effect].outputFromEffect( - [(L9['strength'], strength)], - songTime=now, - # should be counting from when you bumped up from 0 - noteTime=now) - outputSettings.append(out) - - return DeviceSettings.fromList(_graph, outputSettings) - - def save_current_stage(self, subname): - log.info("saving current levels as %s", subname) - with self.graph.currentState() as g: - ds = self.get_output_settings(_graph=g) - effect = L9['effect/%s' % subname] - ctx = URIRef(showconfig.showUri() + '/effect/' + subname) - stmts = ds.statements(effect, ctx, effect + '/', set()) - stmts.extend([ - (effect, RDF.type, L9['Effect'], ctx), - (effect, RDFS.label, Literal(subname), ctx), - (effect, L9['publishAttr'], L9['strength'], ctx), - ]) - - self.graph.suggestPrefixes(ctx, {'eff': effect + '/'}) - self.graph.patch(Patch(addQuads=stmts, delQuads=[])) - - self.sub_name.delete(0, tk.END) - - def alltozero(self): - for uri, subbox in list(self.subbox.items()): - if subbox.scale.scale_var.get() != 0: - subbox.scale.fade(value=0.0, length=0) - - -# move to web lib -def postArgGetter(request): - """return a function that takes arg names and returns string - values. Supports args encoded in the url or in postdata. No - support for repeated args.""" - # this is something nevow normally does for me - request.content.seek(0) - fields = cgi.FieldStorage(request.content, - request.received_headers, - environ={'REQUEST_METHOD': 'POST'}) - - def getArg(n): - try: - return request.args[n][0] - except KeyError: - return fields[n].value - - return getArg - - -class LevelServerHttp(resource.Resource): - isLeaf = True - - def __init__(self, name_to_subbox): - self.name_to_subbox = name_to_subbox - - def render_POST(self, request): - arg = postArgGetter(request) - - if request.path == '/fadesub': - # fadesub?subname=scoop&level=0&secs=.2 - self.name_to_subbox[arg('subname')].scale.fade( - float(arg('level')), float(arg('secs'))) - return "set %s to %s" % (arg('subname'), arg('level')) - else: - raise NotImplementedError(repr(request)) - - -class Sliders(BCF2000): - - def __init__(self, kc): - devices = [ - '/dev/snd/midiC3D0', '/dev/snd/midiC2D0', '/dev/snd/midiC1D0' - ] - for dev in devices: - try: - log.info('try sliders on %s', dev) - BCF2000.__init__(self, dev=dev) - except IOError: - if dev is devices[-1]: - raise - else: - break - - self.kc = kc - log.info('found sliders on %s', dev) - - def valueIn(self, name, value): - kc = self.kc - if name.startswith("slider"): - kc.hw_slider_moved(int(name[6:]) - 1, value / 127) - elif name.startswith("button-upper"): - kc.change_row(kc.current_row) - elif name.startswith("button-lower"): - col = int(name[12:]) - 1 - self.valueOut(name, 0) - try: - tkslider = kc.slider_table[(kc.current_row, col)] - except KeyError: - return - - if tkslider.getVal() == 1.0: - tkslider.setVal(0.0) - else: - tkslider.setVal(1.0) - elif name.startswith("button-corner"): - button_num = int(name[13:]) - 1 - if button_num == 1: - diff = -1 - elif button_num == 3: - diff = 1 - else: - return - - kc.change_row(kc.current_row + diff) - self.valueOut(name, 0) - - -def launch(opts: Any, root: tk.Tk, graph: SyncedGraph, session: URIRef): - tl = toplevelat("Keyboard Composer - %s" % opts.session, - existingtoplevel=root, - graph=graph, - session=session) - - kc = KeyboardComposer(tl, graph, session, hw_sliders=not opts.no_sliders) - kc.pack(fill=tk.BOTH, expand=1) - - for helpline in ["Bindings: B3 mute"]: - tk.Label(root, text=helpline, font="Helvetica -12 italic", - anchor='w').pack(side='top', fill='x') - - -if __name__ == "__main__": - parser = OptionParser() - parser.add_option('--no-sliders', - action='store_true', - help="don't attach to hardware sliders") - clientsession.add_option(parser) - parser.add_option('-v', action='store_true', help="log info level") - opts, args = parser.parse_args() - - log.setLevel(logging.DEBUG if opts.v else logging.INFO) - logging.getLogger('colormath').setLevel(logging.INFO) - - graph = SyncedGraph(networking.rdfdb.url, "keyboardcomposer") - - # i think this also needs delayed start (like subcomposer has), to have a valid graph - # before setting any stuff from the ui - - root = tk.Tk() - initTkdnd(root.tk, 'tkdnd/trunk/') - - session = clientsession.getUri('keyboardcomposer', opts) - - graph.initiallySynced.addCallback(lambda _: launch(opts, root, graph, - session)) - - root.protocol('WM_DELETE_WINDOW', reactor.stop) - - tksupport.install(root, ms=20) - prof.run(reactor.run, profile=None) diff -r 623836db99af -r 4556eebe5d73 bin/lightsim --- a/bin/lightsim Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,138 +0,0 @@ -#!bin/python - -import run_local -import sys, logging - -sys.path.append("lib") -import qt4reactor -qt4reactor.install() - -from twisted.internet import reactor -from twisted.internet.task import LoopingCall -from twisted.web.xmlrpc import Proxy -from louie import dispatcher -from PyQt4.QtGui import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QMainWindow -from OpenGL.GL import * -from OpenGL.GLU import * - -from light9 import networking, Patch, showconfig, dmxclient, updatefreq, prof -from light9.namespaces import L9 -from lightsim.openglsim import Surface - -log = logging.getLogger() -logging.basicConfig( - format= - "%(asctime)s %(levelname)-5s %(name)s %(filename)s:%(lineno)d: %(message)s") -log.setLevel(logging.DEBUG) - - -def filenamesForChan(graph, chan): - for lyr in graph.objects(chan, L9['previewLayer']): - for imgPath in graph.objects(lyr, L9['path']): - yield imgPath - - -_lastLevels = None - - -def poll(graph, serv, pollFreq, oglSurface): - pollFreq.update() - dispatcher.send("status", key="pollFreq", value=str(pollFreq)) - d = serv.callRemote("currentlevels", dmxclient._id) - - def received(dmxLevels): - global _lastLevels - if dmxLevels == _lastLevels: - return - _lastLevels = dmxLevels - - level = {} # filename : level - for i, lev in enumerate(dmxLevels): - if lev == 0: - continue - - try: - chan = Patch.get_channel_uri(Patch.get_channel_name(i + 1)) - except KeyError: - continue - - for imgPath in filenamesForChan(graph, chan): - level[str(imgPath)] = lev - - oglSurface.newLevels(levels=level) - - d.addCallback(received) - return d - - -class StatusKeys(QWidget): - """listens for dispatcher signal 'status' and displays the key/value args""" - - def __init__(self, parent): - QWidget.__init__(self) - self.layout = QVBoxLayout() - self.setLayout(self.layout) - self.row = {} # key name : (Frame, value Label) - dispatcher.connect(self.status, "status") - - def status(self, key, value): - if key not in self.row: - row = QWidget() - self.layout.addWidget(row) - cols = QHBoxLayout() - row.setLayout(cols) - lab1 = QLabel(key) - lab2 = QLabel(value) - cols.addWidget(lab1) - cols.addWidget(lab2) - self.row[key] = lab2 - else: - lab = self.row[key] - lab.setText(value) - - -class Window(QMainWindow): - - def __init__(self, filenames): - QMainWindow.__init__(self, None) - self.setWindowTitle(dmxclient._id) - - w = QWidget() - self.setCentralWidget(w) - mainLayout = QVBoxLayout() - w.setLayout(mainLayout) - - self.glWidget = Surface(self, filenames, imgRescaleTo=128 * 2) - - mainLayout.addWidget(self.glWidget) - - status = StatusKeys(mainLayout) - mainLayout.addWidget(status) - - -def requiredImages(graph): - """filenames that we'll need to show, based on a config structure - like this: - ch:frontLeft a :Channel; - :previewLayer [ :path "lightsim/skyline/front-left.png" ] . - """ - filenames = [] - for lyr in graph.objects(None, L9['previewLayer']): - for p in graph.objects(lyr, L9['path']): - filenames.append(str(p)) - return filenames - - -if __name__ == '__main__': - app = reactor.qApp - - graph = showconfig.getGraph() - - window = Window(requiredImages(graph)) - window.show() - - serv = Proxy(networking.dmxServer.url) - pollFreq = updatefreq.Updatefreq() - LoopingCall(poll, graph, serv, pollFreq, window.glWidget).start(.05) - - reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/listsongs --- a/bin/listsongs Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,28 +0,0 @@ -#!bin/python -"""for completion, print the available song uris on stdout - -in .zshrc: - -function _songs { local expl; _description files expl 'songs'; compadd "$expl[@]" - `${LIGHT9_SHOW}/../../bin/listsongs` } -compdef _songs curvecalc -""" - -from run_local import log # noqa -from twisted.internet import reactor -from rdflib import RDF -from light9 import networking -from light9.namespaces import L9 -from rdfdb.syncedgraph import SyncedGraph - -graph = SyncedGraph(networking.rdfdb.url, "listsongs") - - -@graph.initiallySynced.addCallback -def printSongs(result): - with graph.currentState() as current: - for song in current.subjects(RDF.type, L9['Song']): - print(song) - reactor.stop() - - -reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/live --- a/bin/live Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,2 +0,0 @@ -#!/bin/zsh -exec pnpm exec vite -c light9/live/vite.config.ts diff -r 623836db99af -r 4556eebe5d73 bin/load_test_rdfdb --- a/bin/load_test_rdfdb Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,40 +0,0 @@ -#!bin/python -from run_local import log -from twisted.internet import reactor, task, defer -from rdflib import URIRef, Literal -from twisted.internet.defer import ensureDeferred -from rdfdb.syncedgraph import SyncedGraph -import time, logging - -from light9 import networking, showconfig -from light9.namespaces import L9 - - -class BusyClient: - - def __init__(self, subj, rate): - self.subj = subj - self.rate = rate - - self.graph = SyncedGraph(networking.rdfdb.url, "collector") - self.graph.initiallySynced.addCallback(self.go) - - def go(self, _): - task.LoopingCall(self.loop).start(1 / self.rate) - - def loop(self): - self.graph.patchObject(showconfig.showUri() + '/loadTestContext', - subject=self.subj, - predicate=L9['time'], - newObject=Literal(str(time.time()))) - - -def main(): - log.setLevel(logging.INFO) - - clients = [BusyClient(L9['loadTest_%d' % i], 20) for i in range(10)] - reactor.run() - - -if __name__ == "__main__": - main() diff -r 623836db99af -r 4556eebe5d73 bin/midifade --- a/bin/midifade Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ -#!/bin/sh -exec pdm run python light9/midifade/midifade.py - - diff -r 623836db99af -r 4556eebe5d73 bin/movesinks --- a/bin/movesinks Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,14 +0,0 @@ -#!/bin/bash - -# from http://askubuntu.com/questions/71863/how-to-change-pulseaudio-sink-with-pacmd-set-default-sink-during-playback/113322#113322 - -echo "Setting default sink to: $1"; -pacmd set-default-sink $1 -pacmd list-sink-inputs | grep index | while read line -do -echo "Moving input: "; -echo $line | cut -f2 -d' '; -echo "to sink: $1"; -pacmd move-sink-input `echo $line | cut -f2 -d' '` $1 - -done diff -r 623836db99af -r 4556eebe5d73 bin/mpd_timing_test --- a/bin/mpd_timing_test Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ -#!/usr/bin/python -""" -records times coming out of ascoltami - -for example: - - % mpd_timing_test > timing - # play some music in ascoltami, then ctrl-c - % gnuplot - > plot "timing" with lines - -""" - -import xmlrpc.client, time - -s = xmlrpc.client.ServerProxy("http://localhost:8040") -start = time.time() -while True: - print(time.time() - start, s.gettime()) - time.sleep(.01) diff -r 623836db99af -r 4556eebe5d73 bin/musictime --- a/bin/musictime Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,53 +0,0 @@ -#!bin/python -import run_local # noqa -import light9.networking - -import tkinter as tk -from twisted.internet import reactor, tksupport, task - -from light9.ascoltami.musictime_client import MusicTime - -mt = MusicTime() - - -class MusicTimeTk(tk.Frame, MusicTime): - - def __init__(self, master, url): - tk.Frame.__init__(self) - MusicTime.__init__(self, url) - self.timevar = tk.DoubleVar() - self.timelabel = tk.Label(self, - textvariable=self.timevar, - bd=2, - relief='raised', - width=10, - padx=2, - pady=2, - anchor='w') - self.timelabel.pack(expand=1, fill='both') - - def print_time(evt, *args): - self.timevar.set(mt.getLatest().get('t', 0)) - print(self.timevar.get(), evt.keysym) - - self.timelabel.bind('', print_time) - self.timelabel.bind('<1>', print_time) - self.timelabel.focus() - task.LoopingCall(self.update_time).start(.1) - - def update_time(self): - t = self.getLatest().get('t', 0) - self.timevar.set(t) - - -if __name__ == "__main__": - from optparse import OptionParser - parser = OptionParser() - parser.add_option("-u", "--url", default=light9.networking.musicPlayer.url) - options, args = parser.parse_args() - - root = tk.Tk() - root.title("Time") - MusicTimeTk(root, options.url).pack(expand=1, fill='both') - tksupport.install(root, ms=20) - reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/paintserver --- a/bin/paintserver Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,126 +0,0 @@ -#!bin/python - -from run_local import log -import json -from twisted.internet import reactor -from rdfdb.syncedgraph import SyncedGraph -from light9 import networking, showconfig -import optparse, sys, logging -import cyclone.web -from rdflib import URIRef -from light9 import clientsession -import light9.paint.solve -from cycloneerr import PrettyErrorHandler -from light9.namespaces import L9, DEV -from light9.metrics import metrics -import imp - - -class Solve(PrettyErrorHandler, cyclone.web.RequestHandler): - - def post(self): - painting = json.loads(self.request.body) - with metrics('solve').time(): - img = self.settings.solver.draw(painting) - sample, sampleDist = self.settings.solver.bestMatch( - img, device=DEV['aura2']) - with self.settings.graph.currentState() as g: - bestPath = g.value(sample, L9['imagePath']).replace(L9[''], '') - #out = solver.solve(painting) - #layers = solver.simulationLayers(out) - - self.write( - json.dumps({ - 'bestMatch': { - 'uri': sample, - 'path': bestPath, - 'dist': sampleDist - }, - # 'layers': layers, - # 'out': out, - })) - - def reloadSolver(self): - imp.reload(light9.paint.solve) - self.settings.solver = light9.paint.solve.Solver(self.settings.graph) - self.settings.solver.loadSamples() - - -class BestMatches(PrettyErrorHandler, cyclone.web.RequestHandler): - - def post(self): - body = json.loads(self.request.body) - painting = body['painting'] - devs = [URIRef(d) for d in body['devices']] - with metrics('solve').time(): - img = self.settings.solver.draw(painting) - outSettings = self.settings.solver.bestMatches(img, devs) - self.write(json.dumps({'settings': outSettings.asList()})) - - -class App: - - def __init__(self, show, session): - self.show = show - self.session = session - - self.graph = SyncedGraph(networking.rdfdb.url, "paintServer") - self.graph.initiallySynced.addCallback(self.launch).addErrback( - log.error) - - - def launch(self, *args): - - self.solver = light9.paint.solve.Solver( - self.graph, - sessions=[ - L9['show/dance2017/capture/aura1/cap1876596'], - L9['show/dance2017/capture/aura2/cap1876792'], - L9['show/dance2017/capture/aura3/cap1877057'], - L9['show/dance2017/capture/aura4/cap1877241'], - L9['show/dance2017/capture/aura5/cap1877406'], - L9['show/dance2017/capture/q1/cap1874255'], - L9['show/dance2017/capture/q2/cap1873665'], - L9['show/dance2017/capture/q3/cap1876223'], - ]) - self.solver.loadSamples() - - self.cycloneApp = cyclone.web.Application(handlers=[ - (r'/solve', Solve), - (r'/bestMatches', BestMatches), - metricsRoute(), - ], - debug=True, - graph=self.graph, - solver=self.solver) - reactor.listenTCP(networking.paintServer.port, self.cycloneApp) - log.info("listening on %s" % networking.paintServer.port) - - -if __name__ == "__main__": - parser = optparse.OptionParser() - parser.add_option( - '--show', - help='show URI, like http://light9.bigasterisk.com/show/dance2008', - default=showconfig.showUri()) - parser.add_option("-v", - "--verbose", - action="store_true", - help="logging.DEBUG") - parser.add_option("--twistedlog", - action="store_true", - help="twisted logging") - clientsession.add_option(parser) - (options, args) = parser.parse_args() - log.setLevel(logging.DEBUG if options.verbose else logging.INFO) - - if not options.show: - raise ValueError("missing --show http://...") - - session = clientsession.getUri('paint', options) - - app = App(URIRef(options.show), session) - if options.twistedlog: - from twisted.python import log as twlog - twlog.startLogging(sys.stderr) - reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/patchserver --- a/bin/patchserver Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,83 +0,0 @@ -#!bin/python - -from run_local import log - -from rdflib import URIRef -from twisted.internet import reactor -from twisted.internet.defer import inlineCallbacks, Deferred - -import logging -import optparse -import os -import time -import treq -import cyclone.web, cyclone.websocket, cyclone.httpclient - -from cycloneerr import PrettyErrorHandler - -from light9.namespaces import L9, RDF -from light9 import networking, showconfig -from rdfdb.syncedgraph import SyncedGraph - -from light9.effect.settings import DeviceSettings -from rdfdb.patch import Patch -from light9.metrics import metrics, metricsRoute - - - -def launch(graph): - if 0: - reactor.listenTCP( - networking.captureDevice.port, - cyclone.web.Application(handlers=[ - (r'/()', cyclone.web.StaticFileHandler, { - "path": "light9/web", - "default_filename": "patchServer.html" - }), - metricsRoute(), - ]), - interface='::', - ) - log.info('serving http on %s', networking.captureDevice.port) - - def prn(): - width = {} - for dc in graph.subjects(RDF.type, L9['DeviceClass']): - for attr in graph.objects(dc, L9['attr']): - width[dc] = max( - width.get(dc, 0), - graph.value(attr, L9['dmxOffset']).toPython() + 1) - - user = {} # chan: [dev] - for dev in set(graph.subjects(L9['dmxBase'], None)): - dc = graph.value(dev, RDF.type) - base = graph.value(dev, L9['dmxBase']).toPython() - for offset in range(0, width[dc]): - chan = base + offset - user.setdefault(chan, []).append(dev) - - for chan in range(1, max(user) + 1): - dev = user.get(chan, None) - print(f'chan {chan} used by {dev}') - - graph.addHandler(prn) - - -def main(): - parser = optparse.OptionParser() - parser.add_option("-v", - "--verbose", - action="store_true", - help="logging.DEBUG") - (options, args) = parser.parse_args() - log.setLevel(logging.DEBUG if options.verbose else logging.INFO) - - graph = SyncedGraph(networking.rdfdb.url, "captureDevice") - - graph.initiallySynced.addCallback(lambda _: launch(graph)).addErrback( - log.error) - reactor.run() - - -if __name__ == '__main__': - main() diff -r 623836db99af -r 4556eebe5d73 bin/picamserve --- a/bin/picamserve Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,204 +0,0 @@ -#!env_pi/bin/python - -from run_local import log -import sys -sys.path.append('/usr/lib/python2.7/dist-packages/') -import io, logging, traceback, time -import cyclone.web -from twisted.internet import reactor, threads -from twisted.internet.defer import inlineCallbacks -from light9 import prof - -try: - import picamera - cameraCls = picamera.PiCamera -except ImportError: - - class cameraCls(object): - - def __enter__(self): - return self - - def __exit__(self, *a): - pass - - def capture(self, out, *a, **kw): - out.write(open('yuv.demo').read()) - - def capture_continuous(self, *a, **kw): - for i in range(1000): - time.sleep(1) - yield str(i) - - -def setCameraParams(c, arg): - res = int(arg('res', 480)) - c.resolution = { - 480: (640, 480), - 1080: (1920, 1080), - 1944: (2592, 1944), - }[res] - c.shutter_speed = int(arg('shutter', 50000)) - c.exposure_mode = arg('exposure_mode', 'fixedfps') - c.awb_mode = arg('awb_mode', 'off') - c.brightness = int(arg('brightness', 50)) - c.exposure_compensation = int(arg('exposure_compensation', 0)) - c.awb_gains = (float(arg('redgain', 1)), float(arg('bluegain', 1))) - c.ISO = int(arg('iso', 250)) - c.rotation = int(arg('rotation', '0')) - - -def setupCrop(c, arg): - c.crop = (float(arg('x', 0)), float(arg('y', 0)), float(arg('w', 1)), - float(arg('h', 1))) - rw = rh = int(arg('resize', 100)) - # width 1920, showing w=.3 of image, resize=100 -> scale is 100/.3*1920 - # scl is [ output px / camera px ] - scl1 = rw / (c.crop[2] * c.resolution[0]) - scl2 = rh / (c.crop[3] * c.resolution[1]) - if scl1 < scl2: - # width is the constraint; reduce height to the same scale - rh = int(scl1 * c.crop[3] * c.resolution[1]) - else: - # height is the constraint - rw = int(scl2 * c.crop[2] * c.resolution[0]) - return rw, rh - - -@prof.logTime -def getFrame(c, arg): - setCameraParams(c, arg) - resize = setupCrop(c, arg) - out = io.BytesIO('w') - prof.logTime(c.capture)(out, 'jpeg', use_video_port=True, resize=resize) - return out.getvalue() - - -class Pic(cyclone.web.RequestHandler): - - def get(self): - try: - self.set_header('Content-Type', 'image/jpeg') - self.write(getFrame(self.settings.camera, self.get_argument)) - except Exception: - traceback.print_exc() - - -def captureContinuousAsync(c, resize, onFrame): - """ - Calls c.capture_continuous is called in another thread. onFrame is - called in this reactor thread with each (frameTime, frame) - result. Runs until onFrame raises StopIteration. - """ - - def runner(c, resize): - stream = io.BytesIO() - t = time.time() - for nextFrame in c.capture_continuous(stream, - 'jpeg', - use_video_port=True, - resize=resize): - t2 = time.time() - log.debug(" - framecap got %s bytes in %.1f ms", - len(stream.getvalue()), 1000 * (t2 - t)) - try: - # This is slow, like 13ms. Hopefully - # capture_continuous is working on gathering the next - # pic during this time instead of pausing. - # Instead, we could be stashing frames onto a queue or - # something that the main thread can pull when - # possible (and toss if it gets behind). - threads.blockingCallFromThread(reactor, onFrame, t, - stream.getvalue()) - except StopIteration: - break - t3 = time.time() - log.debug(" - sending to onFrame took %.1fms", 1000 * (t3 - t2)) - stream.truncate() - stream.seek(0) - t = time.time() - - return threads.deferToThread(runner, c, resize) - - -class FpsReport(object): - - def __init__(self): - self.frameTimes = [] - self.lastFpsLog = 0 - - def frame(self): - now = time.time() - - self.frameTimes.append(now) - - if len(self.frameTimes) > 15: - del self.frameTimes[:5] - - if now > self.lastFpsLog + 2 and len(self.frameTimes) > 5: - deltas = [(b - a) - for a, b in zip(self.frameTimes[:-1], self.frameTimes[1:]) - ] - avg = sum(deltas) / len(deltas) - log.info("fps: %.1f", 1 / avg) - self.lastFpsLog = now - - -class Pics(cyclone.web.RequestHandler): - - @inlineCallbacks - def get(self): - try: - self.set_header('Content-Type', 'x-application/length-time-jpeg') - c = self.settings.camera - setCameraParams(c, self.get_argument) - resize = setupCrop(c, self.get_argument) - - self.running = True - log.info("connection open from %s", self.request.remote_ip) - fpsReport = FpsReport() - - def onFrame(frameTime, frame): - if not self.running: - raise StopIteration - - self.write("%s %s\n" % (len(frame), frameTime)) - self.write(frame) - self.flush() - - fpsReport.frame() - - # another camera request coming in at the same time breaks - # the server. it would be nice if this request could - # let-go-and-reopen when it knows about another request - # coming in - yield captureContinuousAsync(c, resize, onFrame) - except Exception: - traceback.print_exc() - - def on_connection_close(self, *a, **kw): - log.info("connection closed") - self.running = False - - -log.setLevel(logging.INFO) - -with cameraCls() as camera: - port = 8208 - reactor.listenTCP( - port, - cyclone.web.Application(handlers=[ - (r'/pic', Pic), - (r'/pics', Pics), - (r'/static/(.*)', cyclone.web.StaticFileHandler, { - 'path': 'light9/web/' - }), - (r'/(|gui.js)', cyclone.web.StaticFileHandler, { - 'path': 'light9/vidref/', - 'default_filename': 'index.html' - }), - ], - debug=True, - camera=camera)) - log.info("serving on %s" % port) - reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/pytest --- a/bin/pytest Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,2 +0,0 @@ -#!/bin/sh -exec pdm run pytest "$@" diff -r 623836db99af -r 4556eebe5d73 bin/python --- a/bin/python Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,2 +0,0 @@ -#!/bin/sh -PYTHONPATH=. pdm run python3 "$@" diff -r 623836db99af -r 4556eebe5d73 bin/run_local.py --- a/bin/run_local.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,8 +0,0 @@ -# this file can go away when all the bin/* are just launchers and everyone uses light9/run_local - -import sys - -# to support 'import light9' -sys.path.append('.') - -from light9.run_local import log diff -r 623836db99af -r 4556eebe5d73 bin/staticclient --- a/bin/staticclient Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,37 +0,0 @@ -#!bin/python -""" -push a dmx level forever -""" - -import time, logging -from optparse import OptionParser -import logging, urllib.request, urllib.parse, urllib.error -from twisted.internet import reactor, tksupport, task -from rdflib import URIRef, RDF, RDFS, Literal - -from run_local import log -log.setLevel(logging.DEBUG) - -from light9 import dmxclient, showconfig, networking - -if __name__ == "__main__": - parser = OptionParser(usage="%prog") - parser.add_option('--chan', help='channel number, starts at 1', - type=int) #todo: or name or uri - parser.add_option('--level', help='0..1', type=float) - parser.add_option('-v', action='store_true', help="log debug level") - - opts, args = parser.parse_args() - - log.setLevel(logging.DEBUG if opts.v else logging.INFO) - - levels = [0] * (opts.chan - 1) + [opts.level] - log.info('staticclient will write this forever: %r', levels) - - def write(): - log.debug('writing %r', levels) - dmxclient.outputlevels(levels, twisted=1) - - log.info('looping...') - task.LoopingCall(write).start(1) - reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/subcomposer --- a/bin/subcomposer Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,314 +0,0 @@ -#!bin/python -""" -subcomposer - session - observable(currentSub), a Submaster which tracks the graph - - EditChoice widget - can change currentSub to another sub - - Levelbox widget - watch observable(currentSub) for a new sub, and also watch currentSub for edits to push to the OneLevel widgets - - OneLevel widget - UI edits are caught here and go all the way back to currentSub - - -""" - -from run_local import log -import time, logging - -log.setLevel(logging.DEBUG) - -from optparse import OptionParser -import logging, urllib.request, urllib.parse, urllib.error -import tkinter as tk -import louie as dispatcher -from twisted.internet import reactor, tksupport, task -from rdflib import URIRef, RDF, RDFS, Literal - -from light9.dmxchanedit import Levelbox -from light9 import dmxclient, Submaster, prof, showconfig, networking -from light9.Patch import get_channel_name -from light9.uihelpers import toplevelat -from rdfdb.syncedgraph import SyncedGraph -from light9 import clientsession -from light9.tkdnd import initTkdnd -from light9.namespaces import L9 -from rdfdb.patch import Patch -from light9.observable import Observable -from light9.editchoice import EditChoice, Local -from light9.subcomposer import subcomposerweb - - -class Subcomposer(tk.Frame): - """ - l9:currentSub ?sub is the URI of the sub we're tied to for displaying and - editing. If we don't have a currentSub, then we're actually - editing a session-local sub called l9:currentSub - - I'm not sure that Locals should even be PersistentSubmaster with - uri and graph storage, but I think that way is making fewer - special cases. - - Contains an EditChoice widget - - Dependencies: - - graph (?session :currentSub ?s) -> self.currentSub - self.currentSub -> graph - self.currentSub -> self._currentChoice (which might be Local) - self._currentChoice (which might be Local) -> self.currentSub - - inside the current sub: - graph -> Submaster levels (handled in Submaster) - Submaster levels -> OneLevel widget - OneLevel widget -> Submaster.editLevel - Submaster.editLevel -> graph (handled in Submaster) - - """ - - def __init__(self, master, graph, session): - tk.Frame.__init__(self, master, bg='black') - self.graph = graph - self.session = session - self.launchTime = time.time() - self.localSerial = 0 - - # this is a URIRef or Local. Strangely, the Local case still - # has a uri, which you can get from - # self.currentSub.uri. Probably that should be on the Local - # object too, or maybe Local should be a subclass of URIRef - self._currentChoice = Observable(Local) - - # this is a PersistentSubmaster (even for local) - self.currentSub = Observable( - Submaster.PersistentSubmaster(graph, self.switchToLocal())) - - def pc(val): - log.info("change viewed sub to %s", val) - - self._currentChoice.subscribe(pc) - - ec = self.editChoice = EditChoice(self, self.graph, self._currentChoice) - ec.frame.pack(side='top') - - ec.subIcon.bind("", self.clickSubIcon) - self.setupSubChoiceLinks() - self.setupLevelboxUi() - - def clickSubIcon(self, *args): - box = tk.Toplevel(self.editChoice.frame) - box.wm_transient(self.editChoice.frame) - tk.Label(box, text="Name this sub:").pack() - e = tk.Entry(box) - e.pack() - b = tk.Button(box, text="Make global") - b.pack() - - def clicked(*args): - self.makeGlobal(newName=e.get()) - box.destroy() - - b.bind("", clicked) - e.focus() - - def makeGlobal(self, newName): - """promote our local submaster into a non-local, named one""" - uri = self.currentSub().uri - newUri = showconfig.showUri() + ("/sub/%s" % - urllib.parse.quote(newName, safe='')) - with self.graph.currentState(tripleFilter=(uri, None, None)) as current: - if (uri, RDF.type, L9['LocalSubmaster']) not in current: - raise ValueError("%s is not a local submaster" % uri) - if (newUri, None, None) in current: - raise ValueError("new uri %s is in use" % newUri) - - # the local submaster was storing in ctx=self.session, but now - # we want it to be in ctx=uri - - self.relocateSub(newUri, newName) - - # these are in separate patches for clarity as i'm debugging this - self.graph.patch( - Patch(addQuads=[ - (newUri, RDFS.label, Literal(newName), newUri), - ], - delQuads=[ - (newUri, RDF.type, L9['LocalSubmaster'], newUri), - ])) - self.graph.patchObject(self.session, self.session, L9['currentSub'], - newUri) - - def relocateSub(self, newUri, newName): - # maybe this goes in Submaster - uri = self.currentSub().uri - - def repl(u): - if u == uri: - return newUri - return u - - delQuads = self.currentSub().allQuads() - addQuads = [(repl(s), p, repl(o), newUri) for s, p, o, c in delQuads] - # patch can't span contexts yet - self.graph.patch(Patch(addQuads=addQuads, delQuads=[])) - self.graph.patch(Patch(addQuads=[], delQuads=delQuads)) - - def setupSubChoiceLinks(self): - graph = self.graph - - def ann(): - print("currently: session=%s currentSub=%r _currentChoice=%r" % - (self.session, self.currentSub(), self._currentChoice())) - - @graph.addHandler - def graphChanged(): - # some bug where SC is making tons of graph edits and many - # are failing. this calms things down. - log.warn('skip graphChanged') - return - - s = graph.value(self.session, L9['currentSub']) - log.debug('HANDLER getting session currentSub from graph: %s', s) - if s is None: - s = self.switchToLocal() - self.currentSub(Submaster.PersistentSubmaster(graph, s)) - - @self.currentSub.subscribe - def subChanged(newSub): - log.debug('HANDLER currentSub changed to %s', newSub) - if newSub is None: - graph.patchObject(self.session, self.session, L9['currentSub'], - None) - return - self.sendupdate() - graph.patchObject(self.session, self.session, L9['currentSub'], - newSub.uri) - - localStmt = (newSub.uri, RDF.type, L9['LocalSubmaster']) - with graph.currentState(tripleFilter=localStmt) as current: - if newSub and localStmt in current: - log.debug(' HANDLER set _currentChoice to Local') - self._currentChoice(Local) - else: - # i think right here is the point that the last local - # becomes garbage, and we could clean it up. - log.debug(' HANDLER set _currentChoice to newSub.uri') - self._currentChoice(newSub.uri) - - dispatcher.connect(self.levelsChanged, "sub levels changed") - - @self._currentChoice.subscribe - def choiceChanged(newChoice): - log.debug('HANDLER choiceChanged to %s', newChoice) - if newChoice is Local: - newChoice = self.switchToLocal() - if newChoice is not None: - newSub = Submaster.PersistentSubmaster(graph, newChoice) - log.debug('write new choice to currentSub, from %r to %r', - self.currentSub(), newSub) - self.currentSub(newSub) - - def levelsChanged(self, sub): - if sub == self.currentSub(): - self.sendupdate() - - def switchToLocal(self): - """ - change our display to a local submaster - """ - # todo: where will these get stored, or are they local to this - # subcomposer process and don't use PersistentSubmaster at all? - localId = "%s-%s" % (self.launchTime, self.localSerial) - self.localSerial += 1 - new = URIRef("http://light9.bigasterisk.com/sub/local/%s" % localId) - log.debug('making up a local sub %s', new) - self.graph.patch( - Patch(addQuads=[ - (new, RDF.type, L9['Submaster'], self.session), - (new, RDF.type, L9['LocalSubmaster'], self.session), - ])) - - return new - - def setupLevelboxUi(self): - self.levelbox = Levelbox(self, self.graph, self.currentSub) - self.levelbox.pack(side='top') - - tk.Button( - self, - text="All to zero", - command=lambda *args: self.currentSub().clear()).pack(side='top') - - def savenewsub(self, subname): - leveldict = {} - for i, lev in zip(list(range(len(self.levels))), self.levels): - if lev != 0: - leveldict[get_channel_name(i + 1)] = lev - - s = Submaster.Submaster(subname, levels=leveldict) - s.save() - - def sendupdate(self): - d = self.currentSub().get_dmx_list() - dmxclient.outputlevels(d, twisted=True) - - -def launch(opts, args, root, graph, session): - if not opts.no_geometry: - toplevelat("subcomposer - %s" % opts.session, root, graph, session) - - sc = Subcomposer(root, graph, session) - sc.pack() - - subcomposerweb.init(graph, session, sc.currentSub) - - tk.Label(root, - text="Bindings: B1 adjust level; B2 set full; B3 instant bump", - font="Helvetica -12 italic", - anchor='w').pack(side='top', fill='x') - - if len(args) == 1: - # it might be a little too weird that cmdline arg to this - # process changes anything else in the same session. But also - # I'm not sure who would ever make 2 subcomposers of the same - # session (except when quitting and restarting, to get the - # same window pos), so maybe it doesn't matter. But still, - # this tool should probably default to making new sessions - # usually instead of loading the same one - graph.patchObject(session, session, L9['currentSub'], URIRef(args[0])) - - task.LoopingCall(sc.sendupdate).start(10) - - -############################# - -if __name__ == "__main__": - parser = OptionParser(usage="%prog [suburi]") - parser.add_option('--no-geometry', - action='store_true', - help="don't save/restore window geometry") - parser.add_option('-v', action='store_true', help="log debug level") - - clientsession.add_option(parser) - opts, args = parser.parse_args() - - log.setLevel(logging.DEBUG if opts.v else logging.INFO) - - root = tk.Tk() - root.config(bg='black') - root.tk_setPalette("#004633") - - initTkdnd(root.tk, 'tkdnd/trunk/') - - graph = SyncedGraph(networking.rdfdb.url, "subcomposer") - session = clientsession.getUri('subcomposer', opts) - - graph.initiallySynced.addCallback(lambda _: launch(opts, args, root, graph, - session)) - - root.protocol('WM_DELETE_WINDOW', reactor.stop) - tksupport.install(root, ms=10) - prof.run(reactor.run, profile=False) diff -r 623836db99af -r 4556eebe5d73 bin/subserver --- a/bin/subserver Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,104 +0,0 @@ -#!bin/python -""" -live web display of all existing subs with pictures, mainly for -dragging them into CC or Timeline -""" -from run_local import log -import optparse, logging, json, subprocess, datetime -from dateutil.tz import tzlocal -from twisted.internet import reactor, defer -import cyclone.web, cyclone.httpclient, cyclone.websocket -from rdflib import URIRef, Literal -import pyjade.utils -from rdfdb.syncedgraph import SyncedGraph -from rdfdb.patch import Patch -from light9.namespaces import L9, DCTERMS -from light9 import networking, showconfig - -from cycloneerr import PrettyErrorHandler - - -class Static(PrettyErrorHandler, cyclone.web.StaticFileHandler): - - def get(self, path, *args, **kw): - if path in ['', 'effects']: - return self.respondStaticJade("light9/subserver/%s.jade" % - (path or 'index')) - - if path.endswith(".js"): - return self.responseStaticCoffee( - 'light9/subserver/%s' % - path.replace(".js", ".coffee")) # potential security hole - - cyclone.web.StaticFileHandler.get(self, path, *args, **kw) - - def respondStaticJade(self, src): - html = pyjade.utils.process(open(src).read()) - self.write(html) - - def responseStaticCoffee(self, src): - self.write( - subprocess.check_output( - ['/usr/bin/coffee', '--compile', '--print', src])) - - -class Snapshot(PrettyErrorHandler, cyclone.web.RequestHandler): - - @defer.inlineCallbacks - def post(self): - about = URIRef(self.get_argument("about")) - response = yield cyclone.httpclient.fetch( - networking.vidref.path("snapshot"), method="POST", timeout=1) - - snapUri = URIRef(json.loads(response.body)['snapshot']) - # vidref could write about when it was taken, etc. would it be - # better for us to tell vidref where to attach the result in - # the graph, and then it doesn't even have to return anything? - - ctx = showconfig.showUri() + "/snapshots" - - self.settings.graph.patch( - Patch(addQuads=[ - (about, L9['image'], snapUri, ctx), - (snapUri, DCTERMS['created'], - Literal(datetime.datetime.now(tzlocal())), ctx), - ])) - - self.write(json.dumps({'snapshot': snapUri})) - - -def newestImage(subject): - newest = (None, None) - for img in graph.objects(subject, L9['image']): - created = graph.value(img, DCTERMS['created']) - if created > newest[0]: - newest = (created, img) - return newest[1] - - -if __name__ == "__main__": - parser = optparse.OptionParser() - parser.add_option("-v", - "--verbose", - action="store_true", - help="logging.DEBUG") - (options, args) = parser.parse_args() - - log.setLevel(logging.DEBUG if options.verbose else logging.INFO) - - graph = SyncedGraph(networking.rdfdb.url, "subServer") - - port = networking.subServer.port - reactor.listenTCP( - port, - cyclone.web.Application(handlers=[ - (r'/snapshot', Snapshot), - (r'/(.*)', Static, { - "path": "light9/subserver", - "default_filename": "index.jade" - }), - ], - debug=True, - graph=graph)) - log.info("serving on %s" % port) - reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/timeline --- a/bin/timeline Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,3 +0,0 @@ -#!/bin/zsh -pnpm exec vite -c light9/web/timeline/vite.config.ts & -wait diff -r 623836db99af -r 4556eebe5d73 bin/tkdnd_minimal_drop.py --- a/bin/tkdnd_minimal_drop.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,56 +0,0 @@ -#!bin/python -from run_local import log -import tkinter as tk -from light9.tkdnd import initTkdnd, dropTargetRegister -from twisted.internet import reactor, tksupport - -root = tk.Tk() -initTkdnd(root.tk, "tkdnd/trunk/") -label = tk.Label(root, borderwidth=2, relief='groove', padx=10, pady=10) -label.pack() -label.config(text="drop target %s" % label._w) - -frame1 = tk.Frame() -frame1.pack() - -labelInner = tk.Label(frame1, borderwidth=2, relief='groove', padx=10, pady=10) -labelInner.pack(side='left') -labelInner.config(text="drop target inner %s" % labelInner._w) -tk.Label(frame1, text="not a target").pack(side='left') - - -def onDrop(ev): - print("onDrop", ev) - - -def enter(ev): - print('enter', ev) - - -def leave(ev): - print('leave', ev) - - -dropTargetRegister(label, - onDrop=onDrop, - onDropEnter=enter, - onDropLeave=leave, - hoverStyle=dict(background="yellow", relief='groove')) - -dropTargetRegister(labelInner, - onDrop=onDrop, - onDropEnter=enter, - onDropLeave=leave, - hoverStyle=dict(background="yellow", relief='groove')) - - -def prn(): - print("cont", root.winfo_containing(201, 151)) - - -b = tk.Button(root, text="coord", command=prn) -b.pack() - -#tk.mainloop() -tksupport.install(root, ms=10) -reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/tracker --- a/bin/tracker Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,311 +0,0 @@ -#!/usr/bin/python - -import sys -sys.path.append("../../editor/pour") -sys.path.append("../light8") - -from Submaster import Submaster -from skim.zooming import Zooming, Pair -from math import sqrt, sin, cos -from pygame.rect import Rect -from xmlnodebase import xmlnodeclass, collectiveelement, xmldocfile -from dispatch import dispatcher - -import dmxclient - -import tkinter as tk - -defaultfont = "arial 8" - - -def pairdist(pair1, pair2): - return pair1.dist(pair2) - - -def canvashighlighter(canvas, obj, attribute, normalval, highlightval): - """creates bindings on a canvas obj that make attribute go - from normal to highlight when the mouse is over the obj""" - canvas.tag_bind( - obj, "", lambda ev: canvas.itemconfig( - obj, **{attribute: highlightval})) - canvas.tag_bind( - obj, - "", lambda ev: canvas.itemconfig(obj, **{attribute: normalval})) - - -class Field(xmlnodeclass): - """one light has a field of influence. for any point on the - canvas, you can ask this field how strong it is. """ - - def name(self, newval=None): - """light/sub name""" - return self._getorsetattr("name", newval) - - def center(self, x=None, y=None): - """x,y float coords for the center of this light in the field. returns - a Pair, although it accepts x,y""" - return Pair(self._getorsettypedattr("x", float, x), - self._getorsettypedattr("y", float, y)) - - def falloff(self, dist=None): - """linear falloff from 1 at center, to 0 at dist pixels away - from center""" - return self._getorsettypedattr("falloff", float, dist) - - def getdistforintensity(self, intens): - """returns the distance you'd have to be for the given intensity (0..1)""" - return (1 - intens) * self.falloff() - - def calc(self, x, y): - """returns field strength at point x,y""" - dist = pairdist(Pair(x, y), self.center()) - return max(0, (self.falloff() - dist) / self.falloff()) - - -class Fieldset(collectiveelement): - """group of fields. persistent.""" - - def childtype(self): - return Field - - def version(self): - """read-only version attribute on fieldset tag""" - return self._getorsetattr("version", None) - - def report(self, x, y): - """reports active fields and their intensities""" - active = 0 - for f in self.getall(): - name = f.name() - intens = f.calc(x, y) - if intens > 0: - print(name, intens, end=' ') - active += 1 - if active > 0: - print() - self.dmxsend(x, y) - - def dmxsend(self, x, y): - """output lights to dmx""" - levels = dict([(f.name(), f.calc(x, y)) for f in self.getall()]) - dmxlist = Submaster(None, levels).get_dmx_list() - dmxclient.outputlevels(dmxlist) - - def getbounds(self): - """returns xmin,xmax,ymin,ymax for the non-zero areas of this field""" - r = None - for f in self.getall(): - rad = f.getdistforintensity(0) - fx, fy = f.center() - fieldrect = Rect(fx - rad, fy - rad, rad * 2, rad * 2) - if r is None: - r = fieldrect - else: - r = r.union(fieldrect) - return r.left, r.right, r.top, r.bottom - - -class Fieldsetfile(xmldocfile): - - def __init__(self, filename): - self._openornew(filename, topleveltype=Fieldset) - - def fieldset(self): - return self._gettoplevel() - - -######################################################################## -######################################################################## - - -class FieldDisplay: - """the view for a Field.""" - - def __init__(self, canvas, field): - self.canvas = canvas - self.field = field - self.tags = [str(id(self))] # canvas tag to id our objects - - def setcoords(self): - """adjust canvas obj coords to match the field""" - # this uses the canvas object ids saved by makeobjs - f = self.field - c = self.canvas - w2c = self.canvas.world2canvas - - # rings - for intens, ring in list(self.rings.items()): - rad = f.getdistforintensity(intens) - p1 = w2c(*(f.center() - Pair(rad, rad))) - p2 = w2c(*(f.center() + Pair(rad, rad))) - c.coords(ring, p1[0], p1[1], p2[0], p2[1]) - - # text - p1 = w2c(*f.center()) - c.coords(self.txt, *p1) - - def makeobjs(self): - """(re)create the canvas objs (null coords) and make their bindings""" - c = self.canvas - f = self.field - c.delete(self.tags) - - w2c = self.canvas.world2canvas - - # make rings - self.rings = {} # rad,canvasobj - for intens, color in ( #(1,'white'), - (.8, 'gray90'), (.6, 'gray80'), (.4, 'gray60'), (.2, 'gray50'), - (0, '#000080')): - self.rings[intens] = c.create_oval(0, - 0, - 0, - 0, - outline=color, - width=2, - tags=self.tags, - outlinestipple='gray50') - - # make text - self.txt = c.create_text(0, - 0, - text=f.name(), - font=defaultfont + " bold", - fill='white', - anchor='c', - tags=self.tags) - - # highlight text bindings - canvashighlighter(c, - self.txt, - 'fill', - normalval='white', - highlightval='red') - - # position drag bindings - def press(ev): - self._lastmouse = ev.x, ev.y - - def motion(ev): - dcan = Pair(*[a - b for a, b in zip((ev.x, ev.y), self._lastmouse)]) - dworld = c.canvas2world_vector(*dcan) - self.field.center(*(self.field.center() + dworld)) - self._lastmouse = ev.x, ev.y - self.setcoords() # redraw - - def release(ev): - if hasattr(self, '_lastmouse'): - del self._lastmouse - dispatcher.send("field coord changed") # updates bounds - - c.tag_bind(self.txt, "", press) - c.tag_bind(self.txt, "", motion) - c.tag_bind(self.txt, "", release) - - # radius drag bindings - outerring = self.rings[0] - canvashighlighter(c, - outerring, - 'outline', - normalval='#000080', - highlightval='#4040ff') - - def motion(ev): - worldmouse = self.canvas.canvas2world(ev.x, ev.y) - currentdist = pairdist(worldmouse, self.field.center()) - self.field.falloff(currentdist) - self.setcoords() - - c.tag_bind(outerring, "", motion) - c.tag_bind(outerring, "", release) # from above - - self.setcoords() - - -class Tracker(tk.Frame): - """whole tracker widget, which is mostly a view for a - Fieldset. tracker makes its own fieldset""" - - # world coords of the visible canvas (preserved even in window resizes) - xmin = 0 - xmax = 100 - ymin = 0 - ymax = 100 - - fieldsetfile = None - displays = None # Field : FieldDisplay. we keep these in sync with the fieldset - - def __init__(self, master): - tk.Frame.__init__(self, master) - - self.displays = {} - - c = self.canvas = Zooming(self, bg='black', closeenough=5) - c.pack(fill='both', exp=1) - - # preserve edge coords over window resize - c.bind("", self.configcoords) - - c.bind("", lambda ev: self._fieldset().report(*c.canvas2world( - ev.x, ev.y))) - - def save(ev): - print("saving") - self.fieldsetfile.save() - - master.bind("", save) - dispatcher.connect(self.autobounds, "field coord changed") - - def _fieldset(self): - return self.fieldsetfile.fieldset() - - def load(self, filename): - self.fieldsetfile = Fieldsetfile(filename) - self.displays.clear() - for f in self.fieldsetfile.fieldset().getall(): - self.displays[f] = FieldDisplay(self.canvas, f) - self.displays[f].makeobjs() - self.autobounds() - - def configcoords(self, *args): - # force our canvas coords to stay at the edges of the window - c = self.canvas - cornerx, cornery = c.canvas2world(0, 0) - c.move(cornerx - self.xmin, cornery - self.ymin) - c.setscale(0, 0, - c.winfo_width() / (self.xmax - self.xmin), - c.winfo_height() / (self.ymax - self.ymin)) - - def autobounds(self): - """figure out our bounds from the fieldset, and adjust the display zooms. - writes the corner coords onto the canvas.""" - self.xmin, self.xmax, self.ymin, self.ymax = self._fieldset().getbounds( - ) - - self.configcoords() - - c = self.canvas - c.delete('cornercoords') - for x, anc2 in ((self.xmin, 'w'), (self.xmax, 'e')): - for y, anc1 in ((self.ymin, 'n'), (self.ymax, 's')): - pos = c.world2canvas(x, y) - c.create_text(pos[0], - pos[1], - text="%s,%s" % (x, y), - fill='white', - anchor=anc1 + anc2, - tags='cornercoords') - [d.setcoords() for d in list(self.displays.values())] - - -######################################################################## -######################################################################## - -root = tk.Tk() -root.wm_geometry('700x350') -tra = Tracker(root) -tra.pack(fill='both', exp=1) - -tra.load("fieldsets/demo") - -root.mainloop() diff -r 623836db99af -r 4556eebe5d73 bin/vidref --- a/bin/vidref Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,188 +0,0 @@ -#!bin/python -""" -Camera images of the stage. View live on a web page and also save -them to disk. Retrieve images based on the song and time that was -playing when they were taken. Also, save snapshot images to a place -they can be used again as thumbnails of effects. - -bin/vidref main -light9/vidref/videorecorder.py capture frames and save them -light9/vidref/replay.py backend for vidref.js playback element- figures out which frames go with the current song and time -light9/vidref/index.html web ui for watching current stage and song playback -light9/vidref/setup.html web ui for setup of camera params and frame crop -light9/web/light9-vidref-live.js LitElement for live video frames -light9/web/light9-vidref-playback.js LitElement for video playback - -""" -from run_local import log - -from typing import cast -import logging, optparse, json, base64, os, glob - -from light9.metrics import metrics, metricsRoute - -from rdflib import URIRef -from twisted.internet import reactor, defer -import cyclone.web, cyclone.httpclient, cyclone.websocket - -from cycloneerr import PrettyErrorHandler -from light9 import networking, showconfig -from light9.newtypes import Song -from light9.vidref import videorecorder -from rdfdb.syncedgraph import SyncedGraph - -parser = optparse.OptionParser() -parser.add_option("-v", "--verbose", action="store_true", help="logging.DEBUG") -(options, args) = parser.parse_args() - -log.setLevel(logging.DEBUG if options.verbose else logging.INFO) - - -class Snapshot(cyclone.web.RequestHandler): - - @defer.inlineCallbacks - def post(self): - # save next pic - # return /snapshot/path - try: - snapshotDir = 'todo' - outputFilename = yield self.settings.gui.snapshot() - - assert outputFilename.startswith(snapshotDir) - out = networking.vidref.path( - "snapshot/%s" % outputFilename[len(snapshotDir):].lstrip('/')) - - self.write(json.dumps({'snapshot': out})) - self.set_header("Location", out) - self.set_status(303) - except Exception: - import traceback - traceback.print_exc() - raise - - -pipeline = videorecorder.GstSource( - #'/dev/v4l/by-id/usb-Bison_HD_Webcam_200901010001-video-index0' - '/dev/v4l/by-id/usb-Generic_FULL_HD_1080P_Webcam_200901010001-video-index0') - - -class Live(cyclone.websocket.WebSocketHandler): - - def connectionMade(self, *args, **kwargs): - pipeline.liveImages.subscribe(on_next=self.onFrame) - metrics('live_clients').offset(1) - - def connectionLost(self, reason): - #self.subj.dispose() - metrics('live_clients').offset(-1) - - def onFrame(self, cf: videorecorder.CaptureFrame): - if cf is None: return - - with metrics('live_websocket_frame_fps').time(): - self.sendMessage( - json.dumps({ - 'jpeg': base64.b64encode(cf.asJpeg()).decode('ascii'), - 'description': f't={cf.t}', - })) - - -class SnapshotPic(cyclone.web.StaticFileHandler): - pass - - -class Time(PrettyErrorHandler, cyclone.web.RequestHandler): - - def put(self): - body = json.loads(self.request.body) - t = body['t'] - for listener in TimeStream.time_stream_listeners: - listener.sendMessage(json.dumps({ - 'st': t, - 'song': body['song'], - })) - self.set_status(202) - - -class TimeStream(cyclone.websocket.WebSocketHandler): - time_stream_listeners = [] - - def connectionMade(self, *args, **kwargs): - TimeStream.time_stream_listeners.append(self) - - def connectionLost(self, reason): - TimeStream.time_stream_listeners.remove(self) - - -class Clips(PrettyErrorHandler, cyclone.web.RequestHandler): - - def delete(self): - clip = URIRef(self.get_argument('uri')) - videorecorder.deleteClip(clip) - - -class ReplayMap(PrettyErrorHandler, cyclone.web.RequestHandler): - - def get(self): - song = Song(self.get_argument('song')) - clips = [] - videoPaths = glob.glob( - os.path.join(videorecorder.songDir(song), b'*.mp4')) - for vid in videoPaths: - pts = [] - for line in open(vid.replace(b'.mp4', b'.timing'), 'rb'): - _v, vt, _eq, _song, st = line.split() - pts.append([float(st), float(vt)]) - - url = vid[len(os.path.dirname(os.path.dirname(showconfig.root())) - ):].decode('ascii') - - clips.append({ - 'uri': videorecorder.takeUri(vid), - 'videoUrl': url, - 'songToVideo': pts - }) - - clips.sort(key=lambda c: len(cast(list, c['songToVideo']))) - clips = clips[-int(self.get_argument('maxClips', '3')):] - clips.sort(key=lambda c: c['uri'], reverse=True) - - ret = json.dumps(clips) - log.info('replayMap had %s videos; json is %s bytes', len(clips), - len(ret)) - self.write(ret) - - -graph = SyncedGraph(networking.rdfdb.url, "vidref") -outVideos = videorecorder.FramesToVideoFiles( - pipeline.liveImages, os.path.join(showconfig.root(), b'video')) - -port = networking.vidref.port -reactor.listenTCP( - port, - cyclone.web.Application( - handlers=[ - (r'/()', cyclone.web.StaticFileHandler, { - 'path': 'light9/vidref', - 'default_filename': 'index.html' - }), - (r'/setup/()', cyclone.web.StaticFileHandler, { - 'path': 'light9/vidref', - 'default_filename': 'setup.html' - }), - (r'/live', Live), - (r'/clips', Clips), - (r'/replayMap', ReplayMap), - (r'/snapshot', Snapshot), - (r'/snapshot/(.*)', SnapshotPic, { - "path": 'todo', - }), - (r'/time', Time), - (r'/time/stream', TimeStream), - metricsRoute(), - ], - debug=True, - )) -log.info("serving on %s" % port) - -reactor.run() diff -r 623836db99af -r 4556eebe5d73 bin/vidrefsetup --- a/bin/vidrefsetup Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,83 +0,0 @@ -#!bin/python -""" this should be part of vidref, but I haven't worked out sharing -camera captures with a continuous camera capture yet """ - -from run_local import log -import optparse, logging -from twisted.internet import reactor -import cyclone.web, cyclone.httpclient, cyclone.websocket -from rdflib import URIRef -from rdfdb.syncedgraph import SyncedGraph -from light9.namespaces import L9 -from light9 import networking, showconfig - -from cycloneerr import PrettyErrorHandler - - -class RedirToCamera(PrettyErrorHandler, cyclone.web.RequestHandler): - - def get(self): - return self.redirect( - networking.picamserve.path('pic?' + self.request.query)) - - -class UrlToCamera(PrettyErrorHandler, cyclone.web.RequestHandler): - - def get(self): - self.set_header('Content-Type', 'text/plain') - self.write(networking.picamserve.path('pic')) - - -class VidrefCamRequest(PrettyErrorHandler, cyclone.web.RequestHandler): - - def get(self): - graph = self.settings.graph - show = showconfig.showUri() - with graph.currentState(tripleFilter=(show, None, None)) as g: - ret = g.value(show, L9['vidrefCamRequest']) - if ret is None: - self.send_error(404) - self.redirect(ret) - - def put(self): - graph = self.settings.graph - show = showconfig.showUri() - graph.patchObject(context=URIRef(show + '/vidrefConfig'), - subject=show, - predicate=L9['vidrefCamRequest'], - newObject=URIRef(self.get_argument('uri'))) - self.send_error(202) - - -def main(): - parser = optparse.OptionParser() - parser.add_option("-v", - "--verbose", - action="store_true", - help="logging.DEBUG") - (options, args) = parser.parse_args() - - log.setLevel(logging.DEBUG if options.verbose else logging.INFO) - graph = SyncedGraph(networking.rdfdb.url, "vidrefsetup") - - # deliberately conflict with vidref since they can't talk at once to cam - port = networking.vidref.port - - reactor.listenTCP( - port, - cyclone.web.Application(handlers=[ - (r'/pic', RedirToCamera), - (r'/picUrl', UrlToCamera), - (r'/vidrefCamRequest', VidrefCamRequest), - (r'/()', cyclone.web.StaticFileHandler, { - 'path': 'light9/vidref/', - 'default_filename': 'vidref.html' - }), - ], - debug=True, - graph=graph)) - log.info("serving on %s" % port) - reactor.run() - - -main() diff -r 623836db99af -r 4556eebe5d73 bin/wavecurve --- a/bin/wavecurve Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,46 +0,0 @@ -#!bin/python -import optparse -from run_local import log -from light9.wavepoints import simp - - -def createCurve(inpath, outpath, t): - print("reading %s, writing %s" % (inpath, outpath)) - points = simp(inpath.replace('.ogg', '.wav'), seconds_per_average=t) - - f = open(outpath, 'w') - for time_val in points: - print("%s %s" % time_val, file=f) - log.info(r'Wrote {outpath}') - - -parser = optparse.OptionParser(usage="""%prog inputSong.wav outputCurve - -You probably just want -a - -""") -parser.add_option("-t", - type="float", - default=.01, - help="seconds per sample (default .01, .07 is smooth)") -parser.add_option("-a", - "--all", - action="store_true", - help="make standard curves for all songs") -options, args = parser.parse_args() - -if options.all: - from light9 import showconfig - from light9.ascoltami.playlist import Playlist - graph = showconfig.getGraph() - - playlist = Playlist.fromShow(showconfig.getGraph(), showconfig.showUri()) - for song in playlist.allSongs(): - inpath = showconfig.songOnDisk(song) - for curveName, t in [('music', .01), ('smooth_music', .07)]: - outpath = showconfig.curvesDir() + "/%s-%s" % ( - showconfig.songFilenameFromURI(song), curveName) - createCurve(inpath, outpath, t) -else: - inpath, outpath = args - createCurve(inpath, outpath, options.t) diff -r 623836db99af -r 4556eebe5d73 bin/webcontrol --- a/bin/webcontrol Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,122 +0,0 @@ -#!bin/python -""" -web UI for various commands that we might want to run from remote -computers and phones - -todo: -disable buttons that don't make sense -""" -import sys, xmlrpc.client, traceback -from twisted.internet import reactor -from twisted.python import log -from twisted.python.util import sibpath -from twisted.internet.defer import inlineCallbacks, returnValue -from twisted.web.client import getPage -from nevow.appserver import NevowSite -from nevow import rend, static, loaders, inevow, url, tags as T -from rdflib import URIRef -from louie.robustapply import robust_apply -sys.path.append(".") -from light9 import showconfig, networking -from light9.namespaces import L9 -from urllib.parse import urlencode - - -# move to web lib -def post(url, **args): - return getPage(url, method='POST', postdata=urlencode(args)) - - -class Commands(object): - - @staticmethod - def playSong(graph, songUri): - s = xmlrpc.client.ServerProxy(networking.musicPlayer.url) - songPath = graph.value(URIRef(songUri), L9.showPath) - if songPath is None: - raise ValueError("unknown song %s" % songUri) - return s.playfile(songPath.encode('ascii')) - - @staticmethod - def stopMusic(graph): - s = xmlrpc.client.ServerProxy(networking.musicPlayer.url) - return s.stop() - - @staticmethod - def worklightsOn(graph): - return post(networking.keyboardComposer.path('fadesub'), - subname='scoop', - level=.5, - secs=.5) - - @staticmethod - def worklightsOff(graph): - return post(networking.keyboardComposer.path('fadesub'), - subname='scoop', - level=0, - secs=.5) - - @staticmethod - def dimmerSet(graph, dimmer, value): - raise NotImplementedError("subcomposer doesnt have an http port yet") - - -class Main(rend.Page): - docFactory = loaders.xmlfile(sibpath(__file__, "../light9/webcontrol.html")) - - def __init__(self, graph): - self.graph = graph - rend.Page.__init__(self) - - def render_status(self, ctx, data): - pic = T.img(src="icon/enabled.png") - if ctx.arg('error'): - pic = T.img(src="icon/warning.png") - return [pic, ctx.arg('status') or 'ready'] - - def render_songButtons(self, ctx, data): - playList = graph.value(show, L9['playList']) - songs = list(graph.items(playList)) - out = [] - for song in songs: - out.append( - T.form(method="post", action="playSong") - [T.input(type='hidden', name='songUri', value=song), - T.button(type='submit')[graph.label(song)]]) - return out - - @inlineCallbacks - def locateChild(self, ctx, segments): - try: - func = getattr(Commands, segments[0]) - req = inevow.IRequest(ctx) - simpleArgDict = dict((k, v[0]) for k, v in list(req.args.items())) - try: - ret = yield robust_apply(func, func, self.graph, - **simpleArgDict) - except KeyboardInterrupt: - raise - except Exception as e: - print("Error on command %s" % segments[0]) - traceback.print_exc() - returnValue((url.here.up().add('status', - str(e)).add('error', - 1), segments[1:])) - - returnValue((url.here.up().add('status', ret), segments[1:])) - #actually return the orig page, with a status message from the func - except AttributeError: - pass - returnValue(rend.Page.locateChild(self, ctx, segments)) - - def child_icon(self, ctx): - return static.File("/usr/share/pyshared/elisa/plugins/poblesec/tango") - - -graph = showconfig.getGraph() -show = showconfig.showUri() - -log.startLogging(sys.stdout) - -reactor.listenTCP(9000, NevowSite(Main(graph))) -reactor.run() diff -r 623836db99af -r 4556eebe5d73 light9/Effects.py --- a/light9/Effects.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,179 +0,0 @@ -import random as random_mod -import math -import logging, colorsys -import light9.Submaster as Submaster -from .chase import chase as chase_logic -from . import showconfig -from rdflib import RDF -from light9 import Patch -from light9.namespaces import L9 -log = logging.getLogger() - -registered = [] - - -def register(f): - registered.append(f) - return f - - -@register -class Strip: - """list of r,g,b tuples for sending to an LED strip""" - which = 'L' # LR means both. W is the wide one - pixels = [] - - def __repr__(self): - return '' % (self.which, self.pixels[0]) - - @classmethod - def solid(cls, which='L', color=(1, 1, 1), hsv=None): - """hsv overrides color""" - if hsv is not None: - color = colorsys.hsv_to_rgb(hsv[0] % 1.0, hsv[1], hsv[2]) - x = cls() - x.which = which - x.pixels = [tuple(color)] * 50 - return x - - def __mul__(self, f): - if not isinstance(f, (int, float)): - raise TypeError - - s = Strip() - s.which = self.which - s.pixels = [(r * f, g * f, b * f) for r, g, b in self.pixels] - return s - - __rmul__ = __mul__ - - -@register -class Blacklight(float): - """a level for the blacklight PWM output""" - - def __mul__(self, f): - return Blacklight(float(self) * f) - - __rmul__ = __mul__ - - -@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_mod.Random(random) - names = names[:] - r.shuffle(names) - - chase_vals = chase_logic(t, ontime, offset, onval, offval, names, combiner) - lev = {} - for uri, value in list(chase_vals.items()): - try: - dmx = Patch.dmx_from_uri(uri) - except KeyError: - log.info(("chase includes %r, which doesn't resolve to a dmx chan" % - uri)) - continue - lev[dmx] = value - - 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 = {} - if light in ['left', 'all']: - lev[73], lev[74], lev[75] = r, g, b - if light in ['right', 'all']: - lev[80], lev[81], lev[82] = r, g, b - 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 - - fade=0 makes steps, fade=1 means each one gets its full fraction - of the time to fade in. Fades never... - """ - frac = 1.0 / len(names) - - lev = {} - for i, uri in enumerate(names): - if t >= (i + 1) * frac: - try: - dmx = Patch.dmx_from_uri(uri) - except KeyError: - log.info( - ("stack includes %r, which doesn't resolve to a dmx chan" % - uri)) - continue - lev[dmx] = 1 - else: - break - - return Submaster.Submaster(name="stack", levels=lev) - - -@register -def smoove(x): - return -2 * (x**3) + 3 * (x**2) - - -def configExprGlobals(): - graph = showconfig.getGraph() - ret = {} - - for chaseUri in graph.subjects(RDF.type, L9['Chase']): - shortName = chaseUri.rsplit('/')[-1] - chans = graph.value(chaseUri, L9['channels']) - ret[shortName] = list(graph.items(chans)) - print("%r is a chase" % shortName) - - 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 - - def nsquare(t, on=.5): - return (t % 1.0) < on - - ret['nsquare'] = nsquare - - _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 diff -r 623836db99af -r 4556eebe5d73 light9/Fadable.py --- a/light9/Fadable.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,165 +0,0 @@ -# taken from SnackMix -- now that's reusable code -import time - - -class Fadable: - """Fading mixin: must mix in with a Tk widget (or something that has - 'after' at least) This is currently used by VolumeBox and MixerTk. - It's probably too specialized to be used elsewhere, but could possibly - work with an Entry or a Meter, I guess. (Actually, this is used by - KeyboardComposer and KeyboardRecorder now too.) - - var is a Tk variable that should be used to set and get the levels. - If use_fades is true, it will use fades to move between levels. - If key_bindings is true, it will install these keybindings: - - Press a number to fade to that amount (e.g. '5' = 50%). Also, - '`' (grave) will fade to 0 and '0' will fade to 100%. - - If mouse_bindings is true, the following mouse bindings will be - installed: Right clicking toggles muting. The mouse wheel will - raise or lower the volume. Shift-mouse wheeling will cause a more - precise volume adjustment. Control-mouse wheeling will cause a - longer fade.""" - - def __init__(self, - var, - wheel_step=5, - use_fades=1, - key_bindings=1, - mouse_bindings=1): - self.use_fades = use_fades # whether increase and decrease should fade - self.wheel_step = wheel_step # amount that increase and descrease should - # change volume (by default) - - self.fade_start_level = 0 - self.fade_end_level = 0 - self.fade_start_time = 0 - self.fade_length = 1 - self.fade_step_time = 10 - self.fade_var = var - self.fading = 0 # whether a fade is in progress - - if key_bindings: - for k in range(1, 10): - self.bind("" % k, lambda evt, k=k: self.fade(k / 10.0)) - self.bind("", lambda evt: self.fade(1.0)) - self.bind("", lambda evt: self.fade(0)) - - # up / down arrows - self.bind("", lambda evt: self.increase()) - self.bind("", lambda evt: self.decrease()) - - if mouse_bindings: - # right mouse button toggles muting - self.bind('<3>', lambda evt: self.toggle_mute()) - # not "NOT ANY MORE!" - homer (i.e. it works again) - - # mouse wheel - self.bind('<4>', lambda evt: self.increase()) - self.bind('<5>', lambda evt: self.decrease()) - - # modified mouse wheel - self.bind('', lambda evt: self.increase(multiplier=0.2)) - self.bind('', lambda evt: self.decrease(multiplier=0.2)) - self.bind('', lambda evt: self.increase(length=1)) - self.bind('', lambda evt: self.decrease(length=1)) - - self.last_level = None # used for muting - - def set_var_rounded(self, value): - """use this instead of just self.fade_var.set(value) so we can - control the precision""" - # this was just to make the display not look so weird, but it - # could actually affect the speed of really slow fades. If - # that's a problem, do a real trace_write hook for the - # variable's display instead of using Label(textvariable=var) - # and format it there. - self.fade_var.set(round(value, 7)) - if self.fade_var.get() != value: - self.fade_var.set(value) - if abs(self.fade_var.get() - value) > .1: - raise ValueError( - "doublevar won't set- trying %r but it stays at %r" % - (value, self.fade_var.get())) - - def fade(self, value, length=0.5, step_time=10): - """Fade to value in length seconds with steps every step_time - milliseconds""" - if length == 0: # 0 seconds fades happen right away and prevents - # and prevents us from entering the fade loop, - # which would cause a divide by zero - self.set_var_rounded(value) - self.fading = 0 # we stop all fades - else: # the general case - self.fade_start_time = time.time() - self.fade_length = length - - self.fade_start_level = self.fade_var.get() - self.fade_end_level = value - - self.fade_step_time = step_time - if not self.fading: - self.fading = 1 - self.do_fade() - - def do_fade(self): - """Actually performs the fade for Fadable.fade. Shouldn't be called - directly.""" - now = time.time() - elapsed = now - self.fade_start_time - complete = elapsed / self.fade_length - complete = min(1.0, complete) - diff = self.fade_end_level - self.fade_start_level - newlevel = (complete * diff) + self.fade_start_level - self.set_var_rounded(newlevel) - if complete < 1: - self.after(self.fade_step_time, self.do_fade) - else: - self.fading = 0 - - def increase(self, multiplier=1, length=0.3): - """Increases the volume by multiplier * wheel_step. If use_fades is - true, it do this as a fade over length time.""" - amount = self.wheel_step * multiplier - if self.fading: - newlevel = self.fade_end_level + amount - else: - newlevel = self.fade_var.get() + amount - newlevel = min(100, newlevel) - self.set_volume(newlevel, length) - - def decrease(self, multiplier=1, length=0.3): - """Descreases the volume by multiplier * wheel_step. If use_fades - is true, it do this as a fade over length time.""" - amount = self.wheel_step * multiplier - if self.fading: - newlevel = self.fade_end_level - amount - else: - newlevel = self.fade_var.get() - amount - newlevel = max(0., newlevel) - self.set_volume(newlevel, length) - - def set_volume(self, newlevel, length=0.3): - """Sets the volume to newlevel, performing a fade of length if - use_fades is true.""" - if self.use_fades: - self.fade(newlevel, length=length) - else: - self.set_var_rounded(newlevel) - - def toggle_mute(self): - """Toggles whether the volume is being muted.""" - if self.last_level is None: - self.last_level = self.fade_var.get() - if self.last_level == 0.: # we don't want last_level to be zero, - # since it will make us toggle between 0 - # and 0 - newlevel = 1. - else: - newlevel = 0. - else: - newlevel = self.last_level - self.last_level = None - - self.set_var_rounded(newlevel) diff -r 623836db99af -r 4556eebe5d73 light9/FlyingFader.py --- a/light9/FlyingFader.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,220 +0,0 @@ -from tkinter import tix -from time import time - - -class Mass: - - def __init__(self): - self.x = 0 # position - self.xgoal = 0 # position goal - - self.v = 0 # velocity - self.maxspeed = .8 # maximum speed, in position/second - self.maxaccel = 3 # maximum acceleration, in position/second^2 - self.eps = .03 # epsilon - numbers within this much are considered the same - - self._lastupdate = time() - self._stopped = 1 - - def equal(self, a, b): - return abs(a - b) < self.eps - - def stop(self): - self.v = 0 - self.xgoal = self.x - self._stopped = 1 - - def update(self): - t0 = self._lastupdate - tnow = time() - self._lastupdate = tnow - - dt = tnow - t0 - - self.x += self.v * dt - # hitting the ends stops the slider - if self.x > 1: - self.v = max(self.v, 0) - self.x = 1 - if self.x < 0: - self.v = min(self.v, 0) - self.x = 0 - - if self.equal(self.x, self.xgoal): - self.x = self.xgoal # clean up value - self.stop() - return - - self._stopped = 0 - dir = (-1.0, 1, 0)[self.xgoal > self.x] - - if abs(self.xgoal - self.x) < abs(self.v * 5 * dt): - # apply the brakes on the last 5 steps - dir *= -.5 - - self.v += dir * self.maxaccel * dt # velocity changes with acceleration in the right direction - self.v = min(max(self.v, -self.maxspeed), - self.maxspeed) # clamp velocity - - #print "x=%+.03f v=%+.03f a=%+.03f %f" % (self.x,self.v,self.maxaccel,self.xgoal) - - def goto(self, newx): - self.xgoal = newx - - def ismoving(self): - return not self._stopped - - -class FlyingFader(tix.Frame): - - def __init__(self, - master, - variable, - label, - fadedur=1.5, - font=('Arial', 8), - labelwidth=12, - **kw): - tix.Frame.__init__(self, master) - self.name = label - self.variable = variable - - self.mass = Mass() - - self.config({'bd': 1, 'relief': 'raised'}) - scaleopts = { - 'variable': variable, - 'showvalue': 0, - 'from': 1.0, - 'to': 0, - 'res': 0.001, - 'width': 20, - 'length': 200, - 'orient': 'vert' - } - scaleopts.update(kw) - if scaleopts['orient'] == 'vert': - side2 = tix.BOTTOM - else: - side2 = tix.LEFT - - self.scale = tix.Scale(self, **scaleopts) - self.vlabel = tix.Label(self, text="0.0", width=6, font=font) - self.label = tix.Label(self, - text=label, - font=font, - anchor='w', - width=labelwidth) #wraplength=40, ) - - self.oldtrough = self.scale['troughcolor'] - - self.scale.pack(side=side2, expand=1, fill=tix.BOTH, anchor='c') - self.vlabel.pack(side=side2, expand=0, fill=tix.X) - self.label.pack(side=side2, expand=0, fill=tix.X) - - for k in range(1, 10): - self.scale.bind( - "" % k, lambda evt, k=k: self.newfade(k / 10.0, evt)) - - self.scale.bind("", lambda evt: self.newfade(1.0, evt)) - self.scale.bind("", lambda evt: self.newfade(0, evt)) - - self.scale.bind("<1>", self.cancelfade) - self.scale.bind("<2>", self.cancelfade) - self.scale.bind("<3>", self.mousefade) - - self.trace_ret = self.variable.trace('w', self.updatelabel) - self.bind("", self.ondestroy) - - def ondestroy(self, *ev): - self.variable.trace_vdelete('w', self.trace_ret) - - def cancelfade(self, evt): - self.fadegoal = self.variable.get() - self.fadevel = self.fadeacc = 0 - - self.scale['troughcolor'] = self.oldtrough - - def mousefade(self, evt): - target = float(self.tk.call(self.scale, 'get', evt.x, evt.y)) - self.newfade(target, evt) - - def ismoving(self): - return self.fadevel != 0 or self.fadeacc != 0 - - def newfade(self, newlevel, evt=None, length=None): - - # these are currently unused-- Mass needs to accept a speed input - mult = 1 - if evt.state & 8 and evt.state & 4: mult = 0.25 # both - elif evt.state & 8: mult = 0.5 # alt - elif evt.state & 4: mult = 2 # control # noqa - - self.mass.x = self.variable.get() - self.mass.goto(newlevel) - - self.gofade() - - def gofade(self): - self.mass.update() - self.variable.set(self.mass.x) - - if not self.mass.ismoving(): - self.scale['troughcolor'] = self.oldtrough - return - - # blink the trough while the thing's moving - if time() % .4 > .2: - # self.scale.config(troughcolor=self.oldtrough) - self.scale.config(troughcolor='orange') - else: - # self.scale.config(troughcolor='white') - self.scale.config(troughcolor='yellow') - -# colorfade(self.scale, percent) - self.after(30, self.gofade) - - def updatelabel(self, *args): - if self.variable: - self.vlabel['text'] = "%.3f" % self.variable.get() - - -# if self.fadetimes[1] == 0: # no fade -# self.vlabel['fg'] = 'black' -# elif self.curfade[1] > self.curfade[0]: -# self.vlabel['fg'] = 'red' -# else: -# self.vlabel['fg'] = 'blue' - - def get(self): - return self.scale.get() - - def set(self, val): - self.scale.set(val) - - -def colorfade(scale, lev): - low = (255, 255, 255) - high = (0, 0, 0) - out = [int(l + lev * (h - l)) for h, l in zip(high, low)] - col = "#%02X%02X%02X" % tuple(out) - scale.config(troughcolor=col) - - -if __name__ == '__main__': - root = tix.Tk() - root.tk_focusFollowsMouse() - - FlyingFader(root, variable=tix.DoubleVar(), - label="suck").pack(side=tix.LEFT, expand=1, fill=tix.BOTH) - FlyingFader(root, variable=tix.DoubleVar(), - label="moof").pack(side=tix.LEFT, expand=1, fill=tix.BOTH) - FlyingFader(root, variable=tix.DoubleVar(), - label="zarf").pack(side=tix.LEFT, expand=1, fill=tix.BOTH) - FlyingFader(root, - variable=tix.DoubleVar(), - label="long name goes here. got it?").pack(side=tix.LEFT, - expand=1, - fill=tix.BOTH) - - root.mainloop() diff -r 623836db99af -r 4556eebe5d73 light9/Patch.py --- a/light9/Patch.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,76 +0,0 @@ -from rdflib import RDF -from light9.namespaces import L9 -from light9 import showconfig - - -def resolve_name(channelname): - "Ensure that we're talking about the primary name of the light." - return get_channel_name(get_dmx_channel(channelname)) - - -def get_all_channels(): - """returns primary names for all channels (sorted)""" - prinames = sorted(list(reverse_patch.values())[:]) - return prinames - - -def get_dmx_channel(name): - if str(name) in patch: - return patch[str(name)] - - try: - i = int(name) - return i - except ValueError: - raise ValueError("Invalid channel name: %r" % name) - - -def get_channel_name(dmxnum): - """if you pass a name, it will get normalized""" - try: - return reverse_patch[dmxnum] - except KeyError: - return str(dmxnum) - - -def get_channel_uri(name): - return uri_map[name] - - -def dmx_from_uri(uri): - return uri_patch[uri] - - -def reload_data(): - global patch, reverse_patch, uri_map, uri_patch - patch = {} - reverse_patch = {} - uri_map = {} - uri_patch = {} - - graph = showconfig.getGraph() - - for chan in graph.subjects(RDF.type, L9['Channel']): - for which, name in enumerate([graph.label(chan)] + - list(graph.objects(chan, L9['altName']))): - name = str(name) - uri_map[name] = chan - - if name in patch: - raise ValueError("channel name %r used multiple times" % name) - for output in graph.objects(chan, L9['output']): - for addr in graph.objects(output, L9['dmxAddress']): - addrInt = int(addr) - patch[name] = addrInt - uri_patch[chan] = addrInt - - if which == 0: - reverse_patch[addrInt] = name - reverse_patch[addr] = name - norm_name = name - else: - reverse_patch[name] = norm_name - - -# importing patch will load initial data -reload_data() diff -r 623836db99af -r 4556eebe5d73 light9/Submaster.py --- a/light9/Submaster.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,391 +0,0 @@ -import logging -from rdflib import Graph, RDF -from rdflib import RDFS, Literal, BNode -from light9.namespaces import L9, XSD -from light9.TLUtility import dict_scale, dict_max -from light9 import showconfig -from light9.Patch import resolve_name, get_dmx_channel, get_channel_uri -from louie import dispatcher -from rdfdb.patch import Patch -log = logging.getLogger('submaster') - - -class Submaster(object): - """mapping of channels to levels""" - - def __init__(self, name, levels): - """this sub has a name just for debugging. It doesn't get persisted. - See PersistentSubmaster. - - levels is a dict - """ - self.name = name - self.levels = levels - - self.temporary = True - - if not self.temporary: - # obsolete - dispatcher.connect(log.error, 'reload all subs') - - #log.debug("%s initial levels %s", self.name, self.levels) - - def _editedLevels(self): - pass - - def set_level(self, channelname, level, save=True): - self.levels[resolve_name(channelname)] = level - self._editedLevels() - - def set_all_levels(self, leveldict): - self.levels.clear() - for k, v in list(leveldict.items()): - # this may call _editedLevels too many times - self.set_level(k, v, save=0) - - def get_levels(self): - return self.levels - - def no_nonzero(self): - return all(v == 0 for v in self.levels.values()) - - def __mul__(self, scalar): - return Submaster("%s*%s" % (self.name, scalar), - levels=dict_scale(self.levels, scalar)) - - __rmul__ = __mul__ - - def max(self, *othersubs): - return sub_maxes(self, *othersubs) - - def __add__(self, other): - return self.max(other) - - def ident(self): - return (self.name, tuple(sorted(self.levels.items()))) - - def __repr__(self): - items = sorted(list(getattr(self, 'levels', {}).items())) - levels = ' '.join(["%s:%.2f" % item for item in items]) - return "<'%s': [%s]>" % (getattr(self, 'name', 'no name yet'), levels) - - def __cmp__(self, other): - # not sure how useful this is - if not isinstance(other, Submaster): - return -1 - return cmp(self.ident(), other.ident()) # noqa - - def __hash__(self): - return hash(self.ident()) - - def get_dmx_list(self): - leveldict = self.get_levels() # gets levels of sub contents - - levels = [] - for k, v in list(leveldict.items()): - if v == 0: - continue - try: - dmxchan = get_dmx_channel(k) - 1 - except ValueError: - log.error( - "error trying to compute dmx levels for submaster %s" % - self.name) - raise - if dmxchan >= len(levels): - levels.extend([0] * (dmxchan - len(levels) + 1)) - levels[dmxchan] = max(v, levels[dmxchan]) - - return levels - - def normalize_patch_names(self): - """Use only the primary patch names.""" - # possibly busted -- don't use unless you know what you're doing - self.set_all_levels(self.levels.copy()) - - def get_normalized_copy(self): - """Get a copy of this sumbaster that only uses the primary patch - names. The levels will be the same.""" - newsub = Submaster("%s (normalized)" % self.name, {}) - newsub.set_all_levels(self.levels) - return newsub - - def crossfade(self, othersub, amount): - """Returns a new sub that is a crossfade between this sub and - another submaster. - - NOTE: You should only crossfade between normalized submasters.""" - otherlevels = othersub.get_levels() - keys_set = {} - for k in list(self.levels.keys()) + list(otherlevels.keys()): - keys_set[k] = 1 - all_keys = list(keys_set.keys()) - - xfaded_sub = Submaster("xfade", {}) - for k in all_keys: - xfaded_sub.set_level( - k, - linear_fade(self.levels.get(k, 0), otherlevels.get(k, 0), - amount)) - - return xfaded_sub - - -class PersistentSubmaster(Submaster): - - def __init__(self, graph, uri): - if uri is None: - raise TypeError("uri must be URIRef") - self.graph, self.uri = graph, uri - self.graph.addHandler(self.setName) - self.graph.addHandler(self.setLevels) - Submaster.__init__(self, self.name, self.levels) - self.temporary = False - - def ident(self): - return self.uri - - def _editedLevels(self): - self.save() - - def changeName(self, newName): - self.graph.patchObject(self.uri, self.uri, RDFS.label, Literal(newName)) - - def setName(self): - log.info("sub update name %s %s", self.uri, self.graph.label(self.uri)) - self.name = self.graph.label(self.uri) - - def setLevels(self): - log.debug("sub update levels") - oldLevels = getattr(self, 'levels', {}).copy() - self.setLevelsFromGraph() - if oldLevels != self.levels: - log.debug("sub %s changed" % self.name) - # dispatcher too? this would help subcomposer - dispatcher.send("sub levels changed", sub=self) - - def setLevelsFromGraph(self): - if hasattr(self, 'levels'): - self.levels.clear() - else: - self.levels = {} - for lev in self.graph.objects(self.uri, L9['lightLevel']): - log.debug(" lightLevel %s %s", self.uri, lev) - chan = self.graph.value(lev, L9['channel']) - - val = self.graph.value(lev, L9['level']) - if val is None: - # broken lightLevel link- may be from someone deleting channels - log.warn("sub %r has lightLevel %r with channel %r " - "and level %r" % (self.uri, lev, chan, val)) - continue - log.debug(" new val %r", val) - if val == 0: - continue - name = self.graph.label(chan) - if not name: - log.error("sub %r has channel %r with no name- " - "leaving out that channel" % (self.name, chan)) - continue - try: - self.levels[name] = float(val) - except Exception: - log.error("name %r val %r" % (name, val)) - raise - - def _saveContext(self): - """the context in which we should save all the lightLevel triples for - this sub""" - typeStmt = (self.uri, RDF.type, L9['Submaster']) - with self.graph.currentState(tripleFilter=typeStmt) as current: - try: - log.debug( - "submaster's type statement is in %r so we save there" % - list(current.contextsForStatement(typeStmt))) - ctx = current.contextsForStatement(typeStmt)[0] - except IndexError: - log.info("declaring %s to be a submaster" % self.uri) - ctx = self.uri - self.graph.patch( - Patch(addQuads=[ - (self.uri, RDF.type, L9['Submaster'], ctx), - ])) - - return ctx - - def editLevel(self, chan, newLevel): - self.graph.patchMapping(self._saveContext(), - subject=self.uri, - predicate=L9['lightLevel'], - nodeClass=L9['ChannelSetting'], - keyPred=L9['channel'], - newKey=chan, - valuePred=L9['level'], - newValue=Literal(newLevel)) - - def clear(self): - """set all levels to 0""" - with self.graph.currentState() as g: - levs = list(g.objects(self.uri, L9['lightLevel'])) - for lev in levs: - self.graph.removeMappingNode(self._saveContext(), lev) - - def allQuads(self): - """all the quads for this sub""" - quads = [] - with self.graph.currentState() as current: - quads.extend(current.quads((self.uri, None, None))) - for s, p, o, c in quads: - if p == L9['lightLevel']: - quads.extend(current.quads((o, None, None))) - return quads - - def save(self): - raise NotImplementedError("obsolete?") - if self.temporary: - log.info("not saving temporary sub named %s", self.name) - return - - graph = Graph() - subUri = L9['sub/%s' % self.name] - graph.add((subUri, RDFS.label, Literal(self.name))) - for chan in list(self.levels.keys()): - try: - chanUri = get_channel_uri(chan) - except KeyError: - log.error("saving dmx channels with no :Channel node " - "is not supported yet. Give channel %s a URI " - "for it to be saved. Omitting this channel " - "from the sub." % chan) - continue - lev = BNode() - graph.add((subUri, L9['lightLevel'], lev)) - graph.add((lev, L9['channel'], chanUri)) - graph.add((lev, L9['level'], - Literal(self.levels[chan], datatype=XSD['decimal']))) - - graph.serialize(showconfig.subFile(self.name), format="nt") - - -def linear_fade(start, end, amount): - """Fades between two floats by an amount. amount is a float between - 0 and 1. If amount is 0, it will return the start value. If it is 1, - the end value will be returned.""" - level = start + (amount * (end - start)) - return level - - -def sub_maxes(*subs): - nonzero_subs = [s for s in subs if not s.no_nonzero()] - name = "max(%s)" % ", ".join([repr(s) for s in nonzero_subs]) - return Submaster(name, - levels=dict_max(*[sub.levels for sub in nonzero_subs])) - - -def combine_subdict(subdict, name=None, permanent=False): - """A subdict is { Submaster objects : levels }. We combine all - submasters first by multiplying the submasters by their corresponding - levels and then max()ing them together. Returns a new Submaster - object. You can give it a better name than the computed one that it - will get or make it permanent if you'd like it to be saved to disk. - Serves 8.""" - scaledsubs = [sub * level for sub, level in list(subdict.items())] - maxes = sub_maxes(*scaledsubs) - if name: - maxes.name = name - if permanent: - maxes.temporary = False - - return maxes - - -class Submasters(object): - "Collection o' Submaster objects" - - def __init__(self, graph): - self.submasters = {} # uri : Submaster - self.graph = graph - - graph.addHandler(self.findSubs) - - def findSubs(self): - current = set() - - for s in self.graph.subjects(RDF.type, L9['Submaster']): - if self.graph.contains((s, RDF.type, L9['LocalSubmaster'])): - continue - log.debug("found sub %s", s) - if s not in self.submasters: - sub = self.submasters[s] = PersistentSubmaster(self.graph, s) - dispatcher.send("new submaster", sub=sub) - current.add(s) - for s in set(self.submasters.keys()) - current: - del self.submasters[s] - dispatcher.send("lost submaster", subUri=s) - log.info("findSubs finished, %s subs", len(self.submasters)) - - def get_all_subs(self): - "All Submaster objects" - v = sorted(list(self.submasters.items())) - v = [x[1] for x in v] - songs = [] - notsongs = [] - for s in v: - if s.name and s.name.startswith('song'): - songs.append(s) - else: - notsongs.append(s) - combined = notsongs + songs - - return combined - - def get_sub_by_uri(self, uri): - return self.submasters[uri] - - def get_sub_by_name(self, name): - return get_sub_by_name(name, self) - - -# a global instance of Submasters, created on demand -_submasters = None - - -def get_global_submasters(graph): - """ - Get (and make on demand) the global instance of - Submasters. Cached, but the cache is not correctly using the graph - argument. The first graph you pass will stick in the cache. - """ - global _submasters - if _submasters is None: - _submasters = Submasters(graph) - return _submasters - - -def get_sub_by_name(name, submasters=None): - """name is a channel or sub nama, submasters is a Submasters object. - If you leave submasters empty, it will use the global instance of - Submasters.""" - if not submasters: - submasters = get_global_submasters() - - # get_all_sub_names went missing. needs rework - #if name in submasters.get_all_sub_names(): - # return submasters.get_sub_by_name(name) - - try: - val = int(name) - s = Submaster("#%d" % val, levels={val: 1.0}) - return s - except ValueError: - pass - - try: - subnum = get_dmx_channel(name) - s = Submaster("'%s'" % name, levels={subnum: 1.0}) - return s - except ValueError: - pass - - # make an error sub - return Submaster('%s' % name, levels=ValueError) diff -r 623836db99af -r 4556eebe5d73 light9/TLUtility.py --- a/light9/TLUtility.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,229 +0,0 @@ -"""Collected utility functions, many are taken from Drew's utils.py in -Cuisine CVS and Hiss's Utility.py.""" - -import sys - -__author__ = "David McClosky , " + \ - "Drew Perttula " -__cvsid__ = "$Id: TLUtility.py,v 1.1 2003/05/25 08:25:35 dmcc Exp $" -__version__ = "$Revision: 1.1 $" [11:-2] - - -def make_attributes_from_args(*argnames): - """ - This function simulates the effect of running - self.foo=foo - for each of the given argument names ('foo' in the example just - now). Now you can write: - def __init__(self,foo,bar,baz): - copy_to_attributes('foo','bar','baz') - ... - instead of: - def __init__(self,foo,bar,baz): - self.foo=foo - self.bar=bar - self.baz=baz - ... - """ - - callerlocals = sys._getframe(1).f_locals - callerself = callerlocals['self'] - for a in argnames: - try: - setattr(callerself, a, callerlocals[a]) - except KeyError: - raise KeyError("Function has no argument '%s'" % a) - - -def enumerate(*collections): - """Generates an indexed series: (0,coll[0]), (1,coll[1]) ... - - this is a multi-list version of the code from the PEP: - enumerate(a,b) gives (0,a[0],b[0]), (1,a[1],b[1]) ... - """ - i = 0 - iters = [iter(collection) for collection in collections] - while True: - yield [ - i, - ] + [next(iterator) for iterator in iters] - i += 1 - - -def dumpobj(o): - """Prints all the object's non-callable attributes""" - print(repr(o)) - for a in [x for x in dir(o) if not callable(getattr(o, x))]: - try: - print(" %20s: %s " % (a, getattr(o, a))) - except Exception: - pass - print("") - - -def dict_filter_update(d, **newitems): - """Adds a set of new keys and values to dictionary 'd' if the values are - true: - - >>> some_dict = {} - >>> dict_filter_update(some_dict, a=None, b=0, c=1, e={}, s='hello') - >>> some_dict - {'c': 1, 's': 'hello'} - """ - for k, v in list(newitems.items()): - if v: d[k] = v - - -def try_get_logger(channel): - """Tries to get a logger with the channel 'channel'. Will return a - silent DummyClass if logging is not available.""" - try: - import logging - log = logging.getLogger(channel) - except ImportError: - log = DummyClass() - return log - - -class DummyClass: - """A class that can be instantiated but never used. It is intended to - be replaced when information is available. - - Usage: - >>> d = DummyClass(1, 2, x="xyzzy") - >>> d.someattr - Traceback (most recent call last): - File "", line 1, in ? - File "Utility.py", line 33, in __getattr__ - raise AttributeError, "Attempted usage of a DummyClass: %s" % key - AttributeError: Attempted usage of a DummyClass: someattr - >>> d.somefunction() - Traceback (most recent call last): - File "", line 1, in ? - File "Utility.py", line 33, in __getattr__ - raise AttributeError, "Attempted usage of a DummyClass: %s" % key - AttributeError: Attempted usage of a DummyClass: somefunction""" - - def __init__(self, use_warnings=1, raise_exceptions=0, **kw): - """Constructs a DummyClass""" - make_attributes_from_args('use_warnings', 'raise_exceptions') - - def __getattr__(self, key): - """Raises an exception to warn the user that a Dummy is not being - replaced in time.""" - if key == "__del__": - return - msg = "Attempted usage of '%s' on a DummyClass" % key - if self.use_warnings: - import warnings - warnings.warn(msg) - if self.raise_exceptions: - raise AttributeError(msg) - return lambda *args, **kw: self.bogus_function() - - def bogus_function(self): - pass - - -class ClassyDict(dict): - """A dict that accepts attribute-style access as well (for keys - that are legal names, obviously). I used to call this Struct, but - chose the more colorful name to avoid confusion with the struct - module.""" - - def __getattr__(self, a): - return self[a] - - def __setattr__(self, a, v): - self[a] = v - - def __delattr__(self, a): - del self[a] - - -def trace(func): - """Good old fashioned Lisp-style tracing. Example usage: - - >>> def f(a, b, c=3): - >>> print a, b, c - >>> return a + b - >>> - >>> - >>> f = trace(f) - >>> f(1, 2) - |>> f called args: [1, 2] - 1 2 3 - <<| f returned 3 - 3 - - TODO: print out default keywords (maybe) - indent for recursive call like the lisp version (possible use of - generators?)""" - name = func.__name__ - - def tracer(*args, **kw): - s = '|>> %s called' % name - if args: - s += ' args: %r' % list(args) - if kw: - s += ' kw: %r' % kw - print(s) - ret = func(*args, **kw) - print('<<| %s returned %s' % (name, ret)) - return ret - - return tracer - - -# these functions taken from old light8 code -def dict_max(*dicts): - """ - ({'a' : 5, 'b' : 9}, {'a' : 10, 'b' : 4}) - returns ==> {'a' : 10, 'b' : 9} - """ - newdict = {} - for d in dicts: - for k, v in list(d.items()): - newdict[k] = max(v, newdict.get(k, 0)) - return newdict - - -def dict_scale(d, scl): - """scales all values in dict and returns a new dict""" - return dict([(k, v * scl) for k, v in d.items()]) - - -def dict_subset(d, dkeys, default=0): - """Subset of dictionary d: only the keys in dkeys. If you plan on omitting - keys, make sure you like the default.""" - newd = {} # dirty variables! - for k in dkeys: - newd[k] = d.get(k, default) - return newd - - -# functions specific to Timeline -# TBD -def last_less_than(array, x): - """array must be sorted""" - best = None - for elt in array: - if elt <= x: - best = elt - elif best is not None: - return best - return best - - -# TBD -def first_greater_than(array, x): - """array must be sorted""" - array_rev = array[:] - array_rev.reverse() - best = None - for elt in array_rev: - if elt >= x: - best = elt - elif best is not None: - return best - return best diff -r 623836db99af -r 4556eebe5d73 light9/__init__.py diff -r 623836db99af -r 4556eebe5d73 light9/ascoltami/__init__.py diff -r 623836db99af -r 4556eebe5d73 light9/ascoltami/main.py --- a/light9/ascoltami/main.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,84 +0,0 @@ -#!bin/python -import logging -import optparse -import sys -from typing import cast - -import gi -from rdflib import URIRef -from starlette.applications import Starlette -from starlette.routing import Route -from starlette_exporter import PrometheusMiddleware, handle_metrics -from twisted.internet import reactor -from twisted.internet.interfaces import IReactorCore - -from light9.run_local import log - -gi.require_version('Gst', '1.0') -gi.require_version('Gtk', '3.0') - -from gi.repository import Gst # type: ignore - -from light9 import networking, showconfig -from light9.ascoltami import webapp -from light9.ascoltami.player import Player -from light9.ascoltami.playlist import NoSuchSong, Playlist - -reactor = cast(IReactorCore, reactor) - - -class Ascoltami: - - def __init__(self, graph, show): - self.graph = graph - self.player = Player(onEOS=self.onEOS, autoStopOffset=0) - self.show = show - self.playlist = Playlist.fromShow(graph, show) - - def onEOS(self, song): - self.player.pause() - self.player.seek(0) - - thisSongUri = webapp.songUri(self.graph, URIRef(song)) - - try: - nextSong = self.playlist.nextSong(thisSongUri) - except NoSuchSong: # we're at the end of the playlist - return - - self.player.setSong(webapp.songLocation(self.graph, nextSong), play=False) - - -def main(): - logging.getLogger('sse_starlette.sse').setLevel(logging.INFO) - Gst.init(None) - - graph = showconfig.getGraph() - asco = Ascoltami(graph, showconfig.showUri()) - - app = Starlette( - debug=True, - routes=[ - Route("/config", webapp.get_config), - Route("/time", webapp.get_time, methods=["GET"]), - Route("/time", webapp.post_time, methods=["POST"]), - Route("/time/stream", webapp.timeStream), - Route("/song", webapp.post_song, methods=["POST"]), - Route("/songs", webapp.get_songs), - Route("/seekPlayOrPause", webapp.post_seekPlayOrPause), - Route("/output", webapp.post_output, methods=["POST"]), - Route("/go", webapp.post_goButton, methods=["POST"]), - ], - ) - - app.add_middleware(PrometheusMiddleware) - app.add_route("/metrics", handle_metrics) - - app.state.graph = graph - app.state.show = asco.show - app.state.player = asco.player - - return app - - -app = main() diff -r 623836db99af -r 4556eebe5d73 light9/ascoltami/main_test.py --- a/light9/ascoltami/main_test.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,7 +0,0 @@ - -from light9.run_local import log - - -def test_import(): - import light9.ascoltami.main - \ No newline at end of file diff -r 623836db99af -r 4556eebe5d73 light9/ascoltami/musictime_client.py --- a/light9/ascoltami/musictime_client.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,98 +0,0 @@ -import time, json, logging -from typing import Dict, cast -from twisted.internet.interfaces import IReactorTime - -from twisted.internet import reactor -from twisted.internet.defer import inlineCallbacks -import treq - -from light9 import networking - -log = logging.getLogger() - - -class MusicTime: - """ - fetch times from ascoltami in a background thread; return times - upon request, adjusted to be more precise with the system clock - """ - - def __init__(self, period=.2, onChange=lambda position: None, pollCurvecalc='ignored'): - """period is the seconds between - http time requests. - - We call onChange with the time in seconds and the total time - - The choice of period doesn't need to be tied to framerate, - it's more the size of the error you can tolerate (since we - make up times between the samples, and we'll just run off the - end of a song) - """ - self.positionFetchTime = 0 - self.period = period - self.hoverPeriod = .05 - self.onChange = onChange - - self.position: Dict[str, float] = {} - # driven by our pollCurvecalcTime and also by Gui.incomingTime - self.lastHoverTime = None # None means "no recent value" - self.pollMusicTime() - - def getLatest(self, frameTime=None) -> Dict: - """ - dict with 't' and 'song', etc. - - frameTime is the timestamp from the camera, which will be used - instead of now. - - Note that this may be called in a gst camera capture thread. Very often. - """ - if not hasattr(self, 'position'): - return {'t': 0, 'song': None} - pos = self.position.copy() - now = frameTime or time.time() - if pos.get('playing'): - pos['t'] = pos['t'] + (now - self.positionFetchTime) - else: - if self.lastHoverTime is not None: - pos['hoverTime'] = self.lastHoverTime - return pos - - def pollMusicTime(self): - - @inlineCallbacks - def cb(response): - - if response.code != 200: - raise ValueError("%s %s", response.code, (yield response.content())) - - position = yield response.json() - - # this is meant to be the time when the server gave me its - # report, and I don't know if that's closer to the - # beginning of my request or the end of it (or some - # fraction of the way through) - self.positionFetchTime = time.time() - - self.position = position - self.onChange(position) - - cast(IReactorTime, reactor).callLater(self.period, self.pollMusicTime) # type: ignore - - def eb(err): - log.warn("talking to ascoltami: %s", err.getErrorMessage()) - cast(IReactorTime, reactor).callLater(2, self.pollMusicTime) # type: ignore - - d = treq.get(networking.musicPlayer.path("time").toPython()) - d.addCallback(cb) - d.addErrback(eb) # note this includes errors in cb() - - def sendTime(self, t): - """request that the player go to this time""" - treq.post( - networking.musicPlayer.path('time'), - data=json.dumps({ - "t": time - }).encode('utf8'), - headers={b"content-type": [b"application/json"]}, - ) diff -r 623836db99af -r 4556eebe5d73 light9/ascoltami/player.py --- a/light9/ascoltami/player.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,195 +0,0 @@ -#!/usr/bin/python -""" -alternate to the mpd music player, for ascoltami -""" - -import time, logging, traceback -from gi.repository import Gst # type: ignore -from twisted.internet import task -from light9.metrics import metrics -log = logging.getLogger() - - - -class Player: - - def __init__(self, autoStopOffset=4, onEOS=None): - """autoStopOffset is the number of seconds before the end of - song before automatically stopping (which is really pausing). - onEOS is an optional function to be called when we reach the - end of a stream (for example, can be used to advance the song). - It is called with one argument which is the URI of the song that - just finished.""" - self.autoStopOffset = autoStopOffset - self.playbin = self.pipeline = Gst.ElementFactory.make('playbin', None) - - self.playStartTime = 0 - self.lastWatchTime = 0 - self.autoStopTime = 0 - self.lastSetSongUri = None - self.onEOS = onEOS - - task.LoopingCall(self.watchTime).start(.050) - - #bus = self.pipeline.get_bus() - # not working- see notes in pollForMessages - #self.watchForMessages(bus) - - def watchTime(self): - try: - self.pollForMessages() - - t = self.currentTime() - log.debug("watch %s < %s < %s", self.lastWatchTime, - self.autoStopTime, t) - if self.lastWatchTime < self.autoStopTime < t: - log.info("autostop") - self.pause() - - self.lastWatchTime = t - except Exception: - traceback.print_exc() - - def watchForMessages(self, bus): - """this would be nicer than pollForMessages but it's not working for - me. It's like add_signal_watch isn't running.""" - bus.add_signal_watch() - - def onEos(*args): - print("onEos", args) - if self.onEOS is not None: - self.onEOS(self.getSong()) - - bus.connect('message::eos', onEos) - - def onStreamStatus(bus, message): - print("streamstatus", bus, message) - (statusType, _elem) = message.parse_stream_status() - if statusType == Gst.StreamStatusType.ENTER: - self.setupAutostop() - - bus.connect('message::stream-status', onStreamStatus) - - def pollForMessages(self): - """bus.add_signal_watch seems to be having no effect, but this works""" - bus = self.pipeline.get_bus() - mt = Gst.MessageType - msg = bus.poll( - mt.EOS | mt.STREAM_STATUS | mt.ERROR, # | mt.ANY, - 0) - if msg is not None: - log.debug("bus message: %r %r", msg.src, msg.type) - # i'm trying to catch here a case where the pulseaudio - # output has an error, since that's otherwise kind of - # mysterious to diagnose. I don't think this is exactly - # working. - if msg.type == mt.ERROR: - log.error(repr(msg.parse_error())) - if msg.type == mt.EOS: - if self.onEOS is not None: - self.onEOS(self.getSong()) - if msg.type == mt.STREAM_STATUS: - (statusType, _elem) = msg.parse_stream_status() - if statusType == Gst.StreamStatusType.ENTER: - self.setupAutostop() - - def seek(self, t): - isSeekable = self.playbin.seek_simple( - Gst.Format.TIME, - Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE | Gst.SeekFlags.SKIP, - t * Gst.SECOND) - if not isSeekable: - raise ValueError('seek_simple failed') - self.playStartTime = time.time() - - def setSong(self, songLoc, play=True): - """ - uri like file:///my/proj/light9/show/dance2010/music/07.wav - """ - log.info("set song to %r" % songLoc) - self.pipeline.set_state(Gst.State.READY) - self.preload(songLoc) - self.pipeline.set_property("uri", songLoc) - self.lastSetSongUri = songLoc - # todo: don't have any error report yet if the uri can't be read - if play: - self.pipeline.set_state(Gst.State.PLAYING) - self.playStartTime = time.time() - - def getSong(self): - """Returns the URI of the current song.""" - # even the 'uri' that I just set isn't readable yet - return self.playbin.get_property("uri") or self.lastSetSongUri - - def preload(self, songPath): - """ - to avoid disk seek stutters, which happened sometimes (in 2007) with the - non-gst version of this program, we read the whole file to get - more OS caching. - - i don't care that it's blocking. - """ - log.info("preloading %s", songPath) - assert songPath.startswith("file://"), songPath - try: - open(songPath[len("file://"):], 'rb').read() - except IOError as e: - log.error("couldn't preload %s, %r", songPath, e) - raise - - @metrics('current_time').time() - def currentTime(self): - success, cur = self.playbin.query_position(Gst.Format.TIME) - if not success: - return 0 - return cur / Gst.SECOND - - def duration(self): - success, dur = self.playbin.query_duration(Gst.Format.TIME) - if not success: - return 0 - return dur / Gst.SECOND - - def states(self): - """json-friendly object describing the interesting states of - the player nodes""" - success, state, pending = self.playbin.get_state(timeout=0) - return { - "current": { - "name": state.value_nick - }, - "pending": { - "name": state.value_nick - } - } - - def pause(self): - self.pipeline.set_state(Gst.State.PAUSED) - - def isAutostopped(self): - """ - are we stopped at the autostop time? - """ - if self.autoStopOffset < .01: - return False - pos = self.currentTime() - autoStop = self.duration() - self.autoStopOffset - return not self.isPlaying() and abs( - pos - autoStop) < 1 # i've seen .4 difference here - - def resume(self): - self.pipeline.set_state(Gst.State.PLAYING) - - def setupAutostop(self): - dur = self.duration() - if dur == 0: - raise ValueError("duration=0, can't set autostop") - self.autoStopTime = (dur - self.autoStopOffset) - log.info("autostop will be at %s", self.autoStopTime) - # pipeline.seek can take a stop time, but using that wasn't - # working out well. I'd get pauses at other times that were - # hard to remove. - - def isPlaying(self): - _, state, _ = self.pipeline.get_state(timeout=0) - return state == Gst.State.PLAYING diff -r 623836db99af -r 4556eebe5d73 light9/ascoltami/playlist.py --- a/light9/ascoltami/playlist.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,55 +0,0 @@ -from light9.showconfig import songOnDisk -from light9.namespaces import L9 - - -class NoSuchSong(ValueError): - """Raised when a song is requested that doesn't exist (e.g. one - after the last song in the playlist).""" - - -class Playlist: - - def __init__(self, graph, playlistUri): - self.graph = graph - self.playlistUri = playlistUri - self.songs = list(graph.items(playlistUri)) - - def nextSong(self, currentSong): - """Returns the next song in the playlist or raises NoSuchSong if - we are at the end of the playlist.""" - try: - currentIndex = self.songs.index(currentSong) - except IndexError: - raise ValueError("%r is not in the current playlist (%r)." % - (currentSong, self.playlistUri)) - - try: - nextSong = self.songs[currentIndex + 1] - except IndexError: - raise NoSuchSong("%r is the last item in the playlist." % - currentSong) - - return nextSong - - def allSongs(self): - """Returns a list of all song URIs in order.""" - return self.songs - - def allSongPaths(self): - """Returns a list of the filesystem paths to all songs in order.""" - paths = [] - for song in self.songs: - paths.append(songOnDisk(song)) - return paths - - def songPath(self, uri): - """filesystem path to a song""" - raise NotImplementedError("see showconfig.songOnDisk") - # maybe that function should be moved to this method - - @classmethod - def fromShow(cls, graph, show): - playlistUri = graph.value(show, L9['playList']) - if not playlistUri: - raise ValueError("%r has no l9:playList" % show) - return cls(graph, playlistUri) diff -r 623836db99af -r 4556eebe5d73 light9/ascoltami/webapp.py --- a/light9/ascoltami/webapp.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,188 +0,0 @@ -import asyncio -import json -import logging -import socket -import subprocess -import time -from typing import cast - -from rdflib import RDFS, Graph, URIRef -from light9.ascoltami.player import Player -from sse_starlette.sse import EventSourceResponse -from starlette.requests import Request -from starlette.responses import JSONResponse, PlainTextResponse - -from light9.namespaces import L9 -from light9.showconfig import getSongsFromShow, showUri, songOnDisk - -log = logging.getLogger() -_songUris = {} # locationUri : song - - -def songLocation(graph, songUri): - loc = URIRef("file://%s" % songOnDisk(songUri)) - _songUris[loc] = songUri - return loc - - -def songUri(graph, locationUri): - return _songUris[locationUri] - - -async def get_config(request: Request) -> JSONResponse: - return JSONResponse( - dict( - host=socket.gethostname(), - show=str(showUri()), - times={ - # these are just for the web display. True values are on Player.__init__ - 'intro': 4, - 'post': 0 - })) - - -def playerSongUri(graph, player): - """or None""" - - playingLocation = player.getSong() - if playingLocation: - return songUri(graph, URIRef(playingLocation)) - else: - return None - - -def currentState(graph, player): - if player.isAutostopped(): - nextAction = 'finish' - elif player.isPlaying(): - nextAction = 'disabled' - else: - nextAction = 'play' - - return { - "song": playerSongUri(graph, player), - "started": player.playStartTime, - "duration": player.duration(), - "playing": player.isPlaying(), - "t": player.currentTime(), - "state": player.states(), - "next": nextAction, - } - - -async def get_time(request: Request) -> JSONResponse: - player = cast(Player, request.app.state.player) - graph = cast(Graph, request.app.state.graph) - return JSONResponse(currentState(graph, player)) - - -async def post_time(request: Request) -> PlainTextResponse: - """ - post a json object with {pause: true} or {resume: true} if you - want those actions. Use {t: } to seek, optionally - with a pause/resume command too. - """ - params = await request.json() - player = cast(Player, request.app.state.player) - if params.get('pause', False): - player.pause() - if params.get('resume', False): - player.resume() - if 't' in params: - player.seek(params['t']) - return PlainTextResponse("ok") - - -async def timeStream(request: Request): - graph = cast(Graph, request.app.state.graph) - player = cast(Player, request.app.state.player) - async def event_generator(): - last_sent = None - last_sent_time = 0.0 - - while True: - now = time.time() - msg = currentState(graph, player) - if msg != last_sent or now > last_sent_time + 2: - event_data = json.dumps(msg) - yield event_data - last_sent = msg - last_sent_time = now - - await asyncio.sleep(0.1) - - return EventSourceResponse(event_generator()) - - -async def get_songs(request: Request) -> JSONResponse: - graph = cast(Graph, request.app.state.graph) - - songs = getSongsFromShow(graph, request.app.state.show) - - songs_data = [ - { # - "uri": s, - "path": graph.value(s, L9['songFilename']), - "label": graph.value(s, RDFS.label) - } for s in songs - ] - - return JSONResponse({"songs": songs_data}) - - -async def post_song(request: Request) -> PlainTextResponse: - """post a uri of song to switch to (and start playing)""" - graph = cast(Graph, request.app.state.graph) - player = cast(Player, request.app.state.player) - - song_uri = URIRef((await request.body()).decode('utf8')) - player.setSong(songLocation(graph, song_uri)) - - return PlainTextResponse("ok") - - -async def post_seekPlayOrPause(request: Request) -> PlainTextResponse: - """curveCalc's ctrl-p or a vidref scrub""" - player = cast(Player, request.app.state.player) - - data = await request.json() - if 'scrub' in data: - player.pause() - player.seek(data['scrub']) - return PlainTextResponse("ok") - if 'action' in data: - if data['action'] == 'play': - player.resume() - elif data['action'] == 'pause': - player.pause() - else: - raise NotImplementedError - return PlainTextResponse("ok") - if player.isPlaying(): - player.pause() - else: - player.seek(data['t']) - player.resume() - - return PlainTextResponse("ok") - - -async def post_output(request: Request) -> PlainTextResponse: - d = await request.json() - subprocess.check_call(["bin/movesinks", str(d['sink'])]) - return PlainTextResponse("ok") - - -async def post_goButton(request: Request) -> PlainTextResponse: - """ - if music is playing, this silently does nothing. - """ - player = cast(Player, request.app.state.player) - - if player.isAutostopped(): - player.resume() - elif player.isPlaying(): - pass - else: - player.resume() - return PlainTextResponse("ok") diff -r 623836db99af -r 4556eebe5d73 light9/ascoltami/webapp_test.py --- a/light9/ascoltami/webapp_test.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,2 +0,0 @@ -# todo -# test that GET /songs doesn't break, etc diff -r 623836db99af -r 4556eebe5d73 light9/chase.py --- a/light9/chase.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,44 +0,0 @@ -def chase(t, - ontime=0.5, - offset=0.2, - onval=1.0, - offval=0.0, - names=None, - combiner=max): - names = names or [] - # maybe this is better: - # period = ontime + ((offset + ontime) * (len(names) - 1)) - period = (offset + ontime) * len(names) - outputs = {} - for index, name in enumerate(names): - # normalize our time - local_offset = (offset + ontime) * index - local_t = t - local_offset - local_t %= period - - # see if we're still in the on part - if local_t <= ontime: - value = onval - else: - value = offval - - # it could be in there twice (in a bounce like (1, 2, 3, 2) - if name in outputs: - outputs[name] = combiner(value, outputs[name]) - else: - outputs[name] = value - return outputs - - -if __name__ == "__main__": - # a little testing - for x in range(80): - x /= 20.0 - output = chase(x, - onval='x', - offval=' ', - ontime=0.1, - offset=0.2, - names=('a', 'b', 'c', 'd')) - output = sorted(list(output.items())) - print("%.2f\t%s" % (x, ' '.join([str(x) for x in output]))) diff -r 623836db99af -r 4556eebe5d73 light9/clientsession.py --- a/light9/clientsession.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ -""" -some clients will support the concept of a named session that keeps -multiple instances of that client separate -""" -from rdflib import URIRef -from urllib.parse import quote -from light9 import showconfig - - -def add_option(parser): - parser.add_option( - '-s', - '--session', - help="name of session used for levels and window position", - default='default') - - -def getUri(appName, opts): - return URIRef("%s/sessions/%s/%s" % - (showconfig.showUri(), appName, quote(opts.session, safe=''))) diff -r 623836db99af -r 4556eebe5d73 light9/coffee.py --- a/light9/coffee.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,23 +0,0 @@ -from cycloneerr import PrettyErrorHandler -import cyclone.web -import subprocess - - -class StaticCoffee(PrettyErrorHandler, cyclone.web.RequestHandler): - """ - e.g. - - (r'/effect\.js', StaticCoffee, { - 'src': 'light9/effecteval/effect.coffee' - }), - """ # noqa - - def initialize(self, src): - super(StaticCoffee, self).initialize() - self.src = src - - def get(self): - self.set_header('Content-Type', 'application/javascript') - self.write( - subprocess.check_output( - ['/usr/bin/coffee', '--compile', '--print', self.src])) diff -r 623836db99af -r 4556eebe5d73 light9/collector/__init__.py diff -r 623836db99af -r 4556eebe5d73 light9/collector/collector.py --- a/light9/collector/collector.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,221 +0,0 @@ -import logging -import time -from typing import Dict, List, Set, Tuple, cast -from light9.typedgraph import typedValue - -from prometheus_client import Summary -from rdfdb.syncedgraph.syncedgraph import SyncedGraph -from rdflib import URIRef - -from light9.collector.device import resolve, toOutputAttrs -from light9.collector.output import Output as OutputInstance -from light9.collector.weblisteners import WebListeners -from light9.effect.settings import DeviceSettings -from light9.namespaces import L9, RDF -from light9.newtypes import (ClientSessionType, ClientType, DeviceAttr, DeviceClass, DeviceSetting, DeviceUri, DmxIndex, DmxMessageIndex, OutputAttr, - OutputRange, OutputUri, OutputValue, UnixTime, VTUnion, uriTail) - -log = logging.getLogger('collector') - -STAT_SETATTR = Summary('set_attr', 'setAttr calls') - -def makeDmxMessageIndex(base: DmxIndex, offset: DmxIndex) -> DmxMessageIndex: - return DmxMessageIndex(base + offset - 1) - - -def _outputMap(graph: SyncedGraph, outputs: Set[OutputUri]) -> Dict[Tuple[DeviceUri, OutputAttr], Tuple[OutputUri, DmxMessageIndex]]: - """From rdf config graph, compute a map of - (device, outputattr) : (output, index) - that explains which output index to set for any device update. - """ - ret = cast(Dict[Tuple[DeviceUri, OutputAttr], Tuple[OutputUri, DmxMessageIndex]], {}) - - for dc in graph.subjects(RDF.type, L9['DeviceClass']): - log.info(' mapping devices of class %s', dc) - for dev in graph.subjects(RDF.type, dc): - dev = cast(DeviceUri, dev) - log.info(' 💡 mapping device %s', dev) - universe = typedValue(OutputUri, graph, dev, L9['dmxUniverse']) - if universe not in outputs: - raise ValueError(f'{dev=} is configured to be in {universe=}, but we have no Output for that universe') - try: - dmxBase = typedValue(DmxIndex, graph, dev, L9['dmxBase']) - except ValueError: - raise ValueError('no :dmxBase for %s' % dev) - - for row in sorted(graph.objects(dc, L9['attr']), key=str): - outputAttr = typedValue(OutputAttr, graph, row, L9['outputAttr']) - offset = typedValue(DmxIndex, graph, row, L9['dmxOffset']) - index = makeDmxMessageIndex(dmxBase, offset) - ret[(dev, outputAttr)] = (universe, index) - log.info(f' {uriTail(outputAttr):15} maps to {uriTail(universe)} index {index}') - return ret - - -class Collector: - """receives setAttrs calls; combines settings; renders them into what outputs like; calls Output.update""" - - def __init__(self, graph: SyncedGraph, outputs: List[OutputInstance], listeners: WebListeners, clientTimeoutSec: float = 10): - self.graph = graph - self.outputs = outputs - self.listeners = listeners - self.clientTimeoutSec = clientTimeoutSec - - self._initTime = time.time() - self._outputByUri: Dict[OutputUri, OutputInstance] = {} - self._deviceType: Dict[DeviceUri, DeviceClass] = {} - self.remapOut: Dict[Tuple[DeviceUri, OutputAttr], OutputRange] = {} - - self.graph.addHandler(self._compile) - - # rename to activeSessons ? - self.lastRequest: Dict[Tuple[ClientType, ClientSessionType], Tuple[UnixTime, Dict[Tuple[DeviceUri, DeviceAttr], VTUnion]]] = {} - - # (dev, devAttr): value to use instead of 0 - self.stickyAttrs: Dict[Tuple[DeviceUri, DeviceAttr], VTUnion] = {} - - def _compile(self): - log.info('Collector._compile:') - self._outputByUri = self._compileOutputByUri() - self._outputMap = _outputMap(self.graph, set(self._outputByUri.keys())) - - self._deviceType.clear() - self.remapOut.clear() - for dc in self.graph.subjects(RDF.type, L9['DeviceClass']): - dc = cast(DeviceClass, dc) - for dev in self.graph.subjects(RDF.type, dc): - dev = cast(DeviceUri, dev) - self._deviceType[dev] = dc - self._compileRemapForDevice(dev) - - def _compileOutputByUri(self) -> Dict[OutputUri, OutputInstance]: - ret = {} - for output in self.outputs: - ret[OutputUri(output.uri)] = output - return ret - - def _compileRemapForDevice(self, dev: DeviceUri): - for remap in self.graph.objects(dev, L9['outputAttrRange']): - attr = typedValue(OutputAttr, self.graph, remap, L9['outputAttr']) - start = typedValue(float, self.graph, remap, L9['start']) - end = typedValue(float, self.graph, remap, L9['end']) - self.remapOut[(dev, attr)] = OutputRange((start, end)) - - @STAT_SETATTR.time() - def setAttrs(self, client: ClientType, clientSession: ClientSessionType, settings: DeviceSettings, sendTime: UnixTime): - """ - Given DeviceSettings, we resolve conflicting values, - process them into output attrs, and call Output.update - to send the new outputs. - - client is a string naming the type of client. - (client, clientSession) is a unique client instance. - clientSession is deprecated. - - Each client session's last settings will be forgotten - after clientTimeoutSec. - """ - # todo: cleanup session code if we really don't want to be able to run multiple sessions of one client - clientSession = ClientSessionType("no_longer_used") - - now = UnixTime(time.time()) - self._warnOnLateRequests(client, now, sendTime) - - self._forgetStaleClients(now) - - self.lastRequest[(client, clientSession)] = (now, self._resolvedSettingsDict(settings)) - - deviceAttrs = self._merge(iter(self.lastRequest.values())) - - outputAttrsByDevice = self._convertToOutputAttrsPerDevice(deviceAttrs) - pendingOut = self._flattenDmxOutput(outputAttrsByDevice) - - t2 = time.time() - - self._updateOutputs(pendingOut) - - t3 = time.time() - if t2 - now > .030 or t3 - t2 > .030: - log.warning("slow setAttrs: prepare %.1fms -> updateOutputs %.1fms" % ((t2 - now) * 1000, (t3 - t2) * 1000)) - - def _warnOnLateRequests(self, client, now, sendTime): - requestLag = now - sendTime - if requestLag > .1 and now > self._initTime + 10 and getattr(self, '_lastWarnTime', 0) < now - 3: - self._lastWarnTime = now - log.warning('collector.setAttrs from %s is running %.1fms after the request was made', client, requestLag * 1000) - - def _forgetStaleClients(self, now): - staleClientSessions = [] - for clientSession, (reqTime, _) in self.lastRequest.items(): - if reqTime < now - self.clientTimeoutSec: - staleClientSessions.append(clientSession) - for clientSession in staleClientSessions: - log.info('forgetting stale client %r', clientSession) - del self.lastRequest[clientSession] - - # todo: move to settings.py - def _resolvedSettingsDict(self, settingsList: DeviceSettings) -> Dict[Tuple[DeviceUri, DeviceAttr], VTUnion]: - out: Dict[Tuple[DeviceUri, DeviceAttr], VTUnion] = {} - for devUri, devAttr, val in settingsList.asList(): - if (devUri, devAttr) in out: - existingVal = out[(devUri, devAttr)] - out[(devUri, devAttr)] = resolve(self._deviceType[devUri], devAttr, [existingVal, val]) - else: - out[(devUri, devAttr)] = val - return out - - def _merge(self, lastRequests): - deviceAttrs: Dict[DeviceUri, Dict[DeviceAttr, VTUnion]] = {} # device: {deviceAttr: value} - for _, lastSettings in lastRequests: - for (device, deviceAttr), value in lastSettings.items(): - if (device, deviceAttr) in self.remapOut: - start, end = self.remapOut[(device, deviceAttr)] - value = start + float(value) * (end - start) - - attrs = deviceAttrs.setdefault(device, {}) - if deviceAttr in attrs: - value = resolve(device, deviceAttr, [attrs[deviceAttr], value]) - attrs[deviceAttr] = value - # list should come from the graph. these are attrs - # that should default to holding the last position, - # not going to 0. - if deviceAttr in [L9['rx'], L9['ry'], L9['zoom'], L9['focus']]: - self.stickyAttrs[(device, deviceAttr)] = cast(float, value) - - # e.g. don't let an unspecified rotation go to 0 - for (d, da), v in self.stickyAttrs.items(): - daDict = deviceAttrs.setdefault(d, {}) - if da not in daDict: - daDict[da] = v - - return deviceAttrs - - def _convertToOutputAttrsPerDevice(self, deviceAttrs): - ret: Dict[DeviceUri, Dict[OutputAttr, OutputValue]] = {} - for d, devType in self._deviceType.items(): - try: - ret[d] = toOutputAttrs(devType, deviceAttrs.get(d, {})) - self.listeners.outputAttrsSet(d, ret[d], self._outputMap) - except Exception as e: - log.error('failing toOutputAttrs on %s: %r', d, e) - return ret - - def _flattenDmxOutput(self, outputAttrs: Dict[DeviceUri, Dict[OutputAttr, OutputValue]]) -> Dict[OutputUri, bytearray]: - pendingOut = cast(Dict[OutputUri, bytearray], {}) - for outUri in self._outputByUri.keys(): - pendingOut[outUri] = bytearray(512) - - for device, attrs in outputAttrs.items(): - for outputAttr, value in attrs.items(): - outputUri, _index = self._outputMap[(device, outputAttr)] - index = DmxMessageIndex(_index) - outArray = pendingOut[outputUri] - if outArray[index] != 0: - log.warning(f'conflict: {outputUri} output array was already nonzero at 0-based index {index}') - raise ValueError(f"someone already wrote to index {index}") - outArray[index] = value - return pendingOut - - def _updateOutputs(self, pendingOut: Dict[OutputUri, bytearray]): - for uri, buf in pendingOut.items(): - self._outputByUri[uri].update(bytes(buf)) diff -r 623836db99af -r 4556eebe5d73 light9/collector/collector_client.py --- a/light9/collector/collector_client.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,12 +0,0 @@ -from light9 import networking -from light9.effect.settings import DeviceSettings -from light9.metrics import metrics -from twisted.internet import defer -from txzmq import ZmqEndpoint, ZmqFactory, ZmqPushConnection -import json, time, logging -import treq - -log = logging.getLogger('coll_client') - - - # d = treq.put(networking.collector.path('attrs'), data=msg, timeout=1) diff -r 623836db99af -r 4556eebe5d73 light9/collector/collector_client_asyncio.py --- a/light9/collector/collector_client_asyncio.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,42 +0,0 @@ -import asyncio -import json -import logging -import time -from light9 import networking -from light9.effect.settings import DeviceSettings -import zmq.asyncio -from prometheus_client import Summary - -log = logging.getLogger('coll_client') - -ZMQ_SEND = Summary('zmq_send', 'calls') - - -def toCollectorJson(client, session, settings: DeviceSettings) -> str: - assert isinstance(settings, DeviceSettings) - return json.dumps({ - 'settings': settings.asList(), - 'client': client, - 'clientSession': session, - 'sendTime': time.time(), - }) - - -class _Sender: - - def __init__(self): - self.context = zmq.asyncio.Context() - self.socket = self.context.socket(zmq.PUB) - self.socket.connect('tcp://127.0.0.1:9203') #todo: tie to :collectorZmq in graph - # old version used: 'tcp://%s:%s' % (service.host, service.port) - - @ZMQ_SEND.time() - async def send(self, client: str, session: str, settings: DeviceSettings): - msg = toCollectorJson(client, session, settings).encode('utf8') - # log.info(f'zmq send {len(msg)}') - await self.socket.send_multipart([b'setAttr', msg]) - - -_sender = _Sender() - -sendToCollector = _sender.send diff -r 623836db99af -r 4556eebe5d73 light9/collector/collector_test.py --- a/light9/collector/collector_test.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,204 +0,0 @@ -import datetime -import time -import unittest - -from freezegun import freeze_time -from light9.effect.settings import DeviceSettings -from rdflib import Namespace - -from light9.collector.collector import Collector -from light9.collector.output import Output -from light9.collector.weblisteners import WebListeners -from light9.mock_syncedgraph import MockSyncedGraph -from light9.namespaces import DEV, L9 -from light9.newtypes import (ClientSessionType, ClientType, DeviceAttr, DeviceUri, HexColor, UnixTime) - -UDMX = Namespace('http://light9.bigasterisk.com/output/udmx/') -DMX0 = Namespace('http://light9.bigasterisk.com/output/dmx0/') - -PREFIX = ''' - @prefix : . - @prefix dev: . - @prefix udmx: . - @prefix dmx0: . -''' - -THEATER = ''' - :brightness a :DeviceAttr; :dataType :scalar . - - :SimpleDimmer a :DeviceClass; - :deviceAttr :brightness; - :attr - [ :outputAttr :level; :dmxOffset 0 ] . - - :ChauvetColorStrip a :DeviceClass; - :deviceAttr :color; - :attr - [ :outputAttr :mode; :dmxOffset 0 ], - [ :outputAttr :red; :dmxOffset 1 ], - [ :outputAttr :green; :dmxOffset 2 ], - [ :outputAttr :blue; :dmxOffset 3 ] . - -''' - -t0 = UnixTime(0) -client1 = ClientType('client1') -client2 = ClientType('client2') -session1 = ClientSessionType('sess1') -session2 = ClientSessionType('sess2') -colorStrip = DeviceUri(DEV['colorStrip']) -inst1 = DeviceUri(DEV['inst1']) -brightness = DeviceAttr(L9['brightness']) -color = DeviceAttr(L9['color']) - - -class MockOutput(Output): - - def __init__(self, uri, connections): - self.connections = connections - self.updates = [] - self.uri = uri - self.numChannels = 4 - - def update(self, values): - self.updates.append(list(values[:self.numChannels])) - - -class MockWebListeners(WebListeners): - - def __init__(self): - "do not init" - - def outputAttrsSet(self, *a, **kw): - pass - - -class TestCollector(unittest.TestCase): - - def setUp(self): - self.graph = MockSyncedGraph(PREFIX + THEATER + ''' - - dev:colorStrip a :Device, :ChauvetColorStrip; - :dmxUniverse udmx:; :dmxBase 1; - :red dev:colorStripRed; - :green dev:colorStripGreen; - :blue dev:colorStripBlue; - :mode dev:colorStripMode . - - dev:inst1 a :Device, :SimpleDimmer; - :dmxUniverse dmx0:; :dmxBase 1; - :level dev:inst1Brightness . - ''') - - self.dmx0 = MockOutput(DMX0, [(0, DMX0['c1'])]) - self.udmx = MockOutput(UDMX, [(0, UDMX['c1']), (1, UDMX['c2']), (2, UDMX['c3']), (3, UDMX['c4'])]) - - def testRoutesColorOutput(self): - c = Collector(self.graph, outputs=[self.dmx0, self.udmx], listeners=MockWebListeners()) - - c.setAttrs(client1, session1, DeviceSettings(self.graph, [(colorStrip, color, HexColor('#00ff00'))]), t0) - - self.assertEqual([ - [215, 0, 255, 0], - ], self.udmx.updates) - self.assertEqual([ - [0, 0, 0, 0], - ], self.dmx0.updates) - - def testOutputMaxOfTwoClients(self): - c = Collector(self.graph, outputs=[self.dmx0, self.udmx], listeners=MockWebListeners()) - - c.setAttrs(client1, session1, DeviceSettings(self.graph, [(colorStrip, color, HexColor('#ff0000'))]), t0) - c.setAttrs(client2, session1, DeviceSettings(self.graph, [(colorStrip, color, HexColor('#333333'))]), t0) - - self.assertEqual([[215, 255, 0, 0], [215, 255, 51, 51]], self.udmx.updates) - self.assertEqual([[0, 0, 0, 0], [0, 0, 0, 0]], self.dmx0.updates) - - def testClientOnSameOutputIsRememberedOverCalls(self): - c = Collector(self.graph, outputs=[self.dmx0, self.udmx], listeners=MockWebListeners()) - - c.setAttrs(client1, session1, DeviceSettings(self.graph, [(colorStrip, color, HexColor('#080000'))]), t0) - c.setAttrs(client2, session1, DeviceSettings(self.graph, [(colorStrip, color, HexColor('#060000'))]), t0) - c.setAttrs(client1, session1, DeviceSettings(self.graph, [(colorStrip, color, HexColor('#050000'))]), t0) - - self.assertEqual([[215, 8, 0, 0], [215, 8, 0, 0], [215, 6, 0, 0]], self.udmx.updates) - self.assertEqual([[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], self.dmx0.updates) - - def testClientsOnDifferentOutputs(self): - c = Collector(self.graph, outputs=[self.dmx0, self.udmx], listeners=MockWebListeners()) - - c.setAttrs(client1, session1, DeviceSettings(self.graph, [(colorStrip, color, HexColor('#aa0000'))]), t0) - c.setAttrs(client2, session1, DeviceSettings(self.graph, [(inst1, brightness, .5)]), t0) - - # ok that udmx is flushed twice- it can screen out its own duplicates - self.assertEqual([[215, 170, 0, 0], [215, 170, 0, 0]], self.udmx.updates) - self.assertEqual([[0, 0, 0, 0], [127, 0, 0, 0]], self.dmx0.updates) - - def testNewSessionReplacesPreviousOutput(self): - # ..as opposed to getting max'd with it - c = Collector(self.graph, outputs=[self.dmx0, self.udmx], listeners=MockWebListeners()) - - c.setAttrs(client1, session1, DeviceSettings(self.graph, [(inst1, brightness, .8)]), t0) - c.setAttrs(client1, session2, DeviceSettings(self.graph, [(inst1, brightness, .5)]), t0) - - self.assertEqual([[204, 0, 0, 0], [127, 0, 0, 0]], self.dmx0.updates) - - def testNewSessionDropsPreviousSettingsOfOtherAttrs(self): - c = Collector(MockSyncedGraph(PREFIX + THEATER + ''' - - dev:colorStrip a :Device, :ChauvetColorStrip; - :dmxUniverse udmx:; :dmxBase 1; - :red dev:colorStripRed; - :green dev:colorStripGreen; - :blue dev:colorStripBlue; - :mode dev:colorStripMode . - - dev:inst1 a :Device, :SimpleDimmer; - :dmxUniverse dmx0:; :dmxBase 0; - :level dev:inst1Brightness . - '''), - outputs=[self.dmx0, self.udmx], - listeners=MockWebListeners()) - - c.setAttrs(client1, session1, DeviceSettings(self.graph, [(colorStrip, color, HexColor('#ff0000'))]), t0) - c.setAttrs(client1, session2, DeviceSettings(self.graph, [(colorStrip, color, HexColor('#00ff00'))]), t0) - - self.assertEqual([[215, 255, 0, 0], [215, 0, 255, 0]], self.udmx.updates) - - def testClientIsForgottenAfterAWhile(self): - with freeze_time(datetime.datetime.now()) as ft: - c = Collector(self.graph, outputs=[self.dmx0, self.udmx], listeners=MockWebListeners()) - c.setAttrs(client1, session1, DeviceSettings(self.graph, [(inst1, brightness, .5)]), UnixTime(time.time())) - ft.tick(delta=datetime.timedelta(seconds=1)) - # this max's with client1's value so we still see .5 - c.setAttrs(client2, session1, DeviceSettings(self.graph, [(inst1, brightness, .2)]), UnixTime(time.time())) - ft.tick(delta=datetime.timedelta(seconds=9.1)) - # now client1 is forgotten, so our value appears - c.setAttrs(client2, session1, DeviceSettings(self.graph, [(inst1, brightness, .4)]), UnixTime(time.time())) - self.assertEqual([[127, 0, 0, 0], [127, 0, 0, 0], [102, 0, 0, 0]], self.dmx0.updates) - - def testClientUpdatesAreNotMerged(self): - # second call to setAttrs forgets the first - c = Collector(self.graph, outputs=[self.dmx0, self.udmx], listeners=MockWebListeners()) - t0 = UnixTime(time.time()) - c.setAttrs(client1, session1, DeviceSettings(self.graph, [(inst1, brightness, .5)]), t0) - c.setAttrs(client1, session1, DeviceSettings(self.graph, [(inst1, brightness, 1)]), t0) - c.setAttrs(client1, session1, DeviceSettings(self.graph, [(colorStrip, color, HexColor('#00ff00'))]), t0) - - self.assertEqual([[215, 0, 0, 0], [215, 0, 0, 0], [215, 0, 255, 0]], self.udmx.updates) - self.assertEqual([[127, 0, 0, 0], [255, 0, 0, 0], [0, 0, 0, 0]], self.dmx0.updates) - - def testRepeatedAttributesInOneRequestGetResolved(self): - c = Collector(self.graph, outputs=[self.dmx0, self.udmx], listeners=MockWebListeners()) - - c.setAttrs(client1, session1, DeviceSettings(self.graph, [ - (inst1, brightness, .5), - (inst1, brightness, .3), - ]), t0) - self.assertEqual([[127, 0, 0, 0]], self.dmx0.updates) - - c.setAttrs(client1, session1, DeviceSettings(self.graph, [ - (inst1, brightness, .3), - (inst1, brightness, .5), - ]), t0) - self.assertEqual([[127, 0, 0, 0], [127, 0, 0, 0]], self.dmx0.updates) diff -r 623836db99af -r 4556eebe5d73 light9/collector/device.py --- a/light9/collector/device.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,260 +0,0 @@ -import logging -from typing import Dict, List, Any, TypeVar, cast -from light9.namespaces import L9 -from rdflib import Literal, URIRef -from webcolors import hex_to_rgb, rgb_to_hex -from colormath.color_objects import sRGBColor, CMYColor -import colormath.color_conversions -from light9.newtypes import VT, DeviceClass, HexColor, OutputAttr, OutputValue, DeviceUri, DeviceAttr, VTUnion - -log = logging.getLogger('device') - - -class Device: - pass - - -class ChauvetColorStrip(Device): - """ - device attrs: - color - """ - - -class Mini15(Device): - """ - plan: - - device attrs - rx, ry - color - gobo - goboShake - imageAim (configured with a file of calibration data) - """ - - -def clamp255(x): - return min(255, max(0, x)) - - -def _8bit(f): - if not isinstance(f, (int, float)): - raise TypeError(repr(f)) - return clamp255(int(f * 255)) - - -def _maxColor(values: List[HexColor]) -> HexColor: - rgbs = [hex_to_rgb(v) for v in values] - maxes = [max(component) for component in zip(*rgbs)] - return cast(HexColor, rgb_to_hex(tuple(maxes))) - - -def resolve( - deviceType: DeviceClass, - deviceAttr: DeviceAttr, - values: List[VTUnion]) -> VTUnion: # todo: return should be VT - """ - return one value to use for this attr, given a set of them that - have come in simultaneously. len(values) >= 1. - - bug: some callers are passing a device instance for 1st arg - """ - if len(values) == 1: - return values[0] - if deviceAttr == DeviceAttr(L9['color']): - return _maxColor(cast(List[HexColor], values)) - # incomplete. how-to-resolve should be on the DeviceAttr defs in the graph. - if deviceAttr in map(DeviceAttr, [L9['rx'], L9['ry'], L9['zoom'], L9['focus'], L9['iris']]): - floatVals = [] - for v in values: - if isinstance(v, Literal): - floatVals.append(float(v.toPython())) - elif isinstance(v, (int, float)): - floatVals.append(float(v)) - else: - raise TypeError(repr(v)) - - # averaging with zeros? not so good - return sum(floatVals) / len(floatVals) - return max(values) - - -def toOutputAttrs( - deviceType: DeviceClass, - deviceAttrSettings: Dict[DeviceAttr, VTUnion # TODO - ]) -> Dict[OutputAttr, OutputValue]: - return dict((OutputAttr(u), OutputValue(v)) for u, v in untype_toOutputAttrs(deviceType, deviceAttrSettings).items()) - - -def untype_toOutputAttrs(deviceType, deviceAttrSettings) -> Dict[URIRef, int]: - """ - Given device attr settings like {L9['color']: Literal('#ff0000')}, - return a similar dict where the keys are output attrs (like - L9['red']) and the values are suitable for Collector.setAttr - - :outputAttrRange happens before we get here. - """ - - def floatAttr(attr, default=0): - out = deviceAttrSettings.get(attr) - if out is None: - return default - return float(out.toPython()) if isinstance(out, Literal) else out - - def rgbAttr(attr): - color = deviceAttrSettings.get(attr, '#000000') - r, g, b = hex_to_rgb(color) - return r, g, b - - def cmyAttr(attr): - rgb = sRGBColor.new_from_rgb_hex(deviceAttrSettings.get(attr, '#000000')) - out = colormath.color_conversions.convert_color(rgb, CMYColor) - return (_8bit(out.cmy_c), _8bit(out.cmy_m), _8bit(out.cmy_y)) - - def fine16Attr(attr, scale=1.0): - x = floatAttr(attr) * scale - hi = _8bit(x) - lo = _8bit((x * 255) % 1.0) - return hi, lo - - def choiceAttr(attr): - # todo - if deviceAttrSettings.get(attr) == L9['g1']: - return 3 - if deviceAttrSettings.get(attr) == L9['g2']: - return 10 - return 0 - - if deviceType == L9['ChauvetColorStrip']: - r, g, b = rgbAttr(L9['color']) - return {L9['mode']: 215, L9['red']: r, L9['green']: g, L9['blue']: b} - elif deviceType == L9['Bar612601d']: - r, g, b = rgbAttr(L9['color']) - return {L9['red']: r, L9['green']: g, L9['blue']: b} - elif deviceType == L9['LedPar90']: - r, g, b = rgbAttr(L9['color']) - return {L9['master']: 255, L9['red']: r, L9['green']: g, L9['blue']: b, L9['white']: 0} - elif deviceType == L9['LedPar54']: - r, g, b = rgbAttr(L9['color']) - return {L9['master']: 255, L9['red']: r, L9['green']: g, L9['blue']: b, L9['white']: 0, L9['strobe']: 0} - elif deviceType == L9['SimpleDimmer']: - return {L9['level']: _8bit(floatAttr(L9['brightness']))} - elif deviceType == L9['MegaFlash']: - return { - L9['brightness']: _8bit(floatAttr(L9['brightness'])), - L9['strobeSpeed']: _8bit(floatAttr(L9['strobeSpeed'])), - } - elif deviceType == L9['Mini15']: - out = { - L9['rotationSpeed']: 0, # seems to have no effect - L9['dimmer']: 255, - L9['colorChange']: 0, - L9['colorSpeed']: 0, - L9['goboShake']: _8bit(floatAttr(L9['goboShake'])), - } - - out[L9['goboChoose']] = { - L9['open']: 0, - L9['mini15Gobo1']: 10, - L9['mini15Gobo2']: 20, - L9['mini15Gobo3']: 30, - }[deviceAttrSettings.get(L9['mini15GoboChoice'], L9['open'])] - - out[L9['red']], out[L9['green']], out[L9['blue']] = rgbAttr(L9['color']) - out[L9['xRotation']], out[L9['xFine']] = fine16Attr(L9['rx'], 1 / 540) - out[L9['yRotation']], out[L9['yFine']] = fine16Attr(L9['ry'], 1 / 240) - # didn't find docs on this, but from tests it looks like 64 fine steps takes you to the next coarse step - - return out - elif deviceType == L9['ChauvetHex12']: - out = {} - out[L9['red']], out[L9['green']], out[L9['blue']] = r, g, b = rgbAttr(L9['color']) - out[L9['amber']] = 0 - out[L9['white']] = min(r, g, b) - out[L9['uv']] = _8bit(floatAttr(L9['uv'])) - return out - elif deviceType == L9['Source4LedSeries2']: - out = {} - out[L9['red']], out[L9['green']], out[L9['blue']] = rgbAttr(L9['color']) - out[L9['strobe']] = 0 - out[L9['fixed255']] = 255 - for num in range(7): - out[L9['fixed128_%s' % num]] = 128 - return out - elif deviceType == L9['MacAura']: - out = { - L9['shutter']: 22, - L9['dimmer']: 255, - L9['zoom']: _8bit(floatAttr(L9['zoom'])), - L9['fixtureControl']: 0, - L9['colorWheel']: 0, - L9['colorTemperature']: 128, - L9['fx1Select']: 0, - L9['fx1Adjust']: 0, - L9['fx2Select']: 0, - L9['fx2Adjust']: 0, - L9['fxSync']: 0, - L9['auraShutter']: 22, - L9['auraDimmer']: 0, - L9['auraColorWheel']: 0, - L9['auraRed']: 0, - L9['auraGreen']: 0, - L9['auraBlue']: 0, - } - out[L9['pan']], out[L9['panFine']] = fine16Attr(L9['rx']) - out[L9['tilt']], out[L9['tiltFine']] = fine16Attr(L9['ry']) - out[L9['red']], out[L9['green']], out[L9['blue']] = rgbAttr(L9['color']) - out[L9['white']] = 0 - - return out - elif deviceType == L9['MacQuantum']: - out = { - L9['dimmerFadeLo']: 0, - L9['fixtureControl']: 0, - L9['fx1Select']: 0, - L9['fx1Adjust']: 0, - L9['fx2Select']: 0, - L9['fx2Adjust']: 0, - L9['fxSync']: 0, - } - - # note these values are set to 'fade', so they update slowly. Haven't found where to turn that off. - out[L9['cyan']], out[L9['magenta']], out[L9['yellow']] = cmyAttr(L9['color']) - - out[L9['focusHi']], out[L9['focusLo']] = fine16Attr(L9['focus']) - out[L9['panHi']], out[L9['panLo']] = fine16Attr(L9['rx']) - out[L9['tiltHi']], out[L9['tiltLo']] = fine16Attr(L9['ry']) - out[L9['zoomHi']], out[L9['zoomLo']] = fine16Attr(L9['zoom']) - out[L9['dimmerFadeHi']] = 0 if deviceAttrSettings.get(L9['color'], '#000000') == '#000000' else 255 - - out[L9['goboChoice']] = { - L9['open']: 0, - L9['spider']: 36, - L9['windmill']: 41, - L9['limbo']: 46, - L9['brush']: 51, - L9['whirlpool']: 56, - L9['stars']: 61, - }[deviceAttrSettings.get(L9['quantumGoboChoice'], L9['open'])] - - # my goboSpeed deviceAttr goes 0=stopped to 1=fastest (using one direction only) - x = .5 + .5 * floatAttr(L9['goboSpeed']) - out[L9['goboSpeedHi']] = _8bit(x) - out[L9['goboSpeedLo']] = _8bit((x * 255) % 1.0) - - strobe = floatAttr(L9['strobe']) - if strobe < .1: - out[L9['shutter']] = 30 - else: - out[L9['shutter']] = 50 + int(150 * (strobe - .1) / .9) - - out.update({ - L9['colorWheel']: 0, - L9['goboStaticRotate']: 0, - L9['prismRotation']: _8bit(floatAttr(L9['prism'])), - L9['iris']: _8bit(floatAttr(L9['iris']) * (200 / 255)), - }) - return out - else: - raise NotImplementedError('device %r' % deviceType) diff -r 623836db99af -r 4556eebe5d73 light9/collector/device_test.py --- a/light9/collector/device_test.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,59 +0,0 @@ -import unittest -from light9.newtypes import DeviceAttr, DeviceClass, HexColor, OutputAttr -from rdflib import Literal -from light9.namespaces import L9 - -from light9.collector.device import toOutputAttrs, resolve - - -class TestUnknownDevice(unittest.TestCase): - - def testFails(self): - self.assertRaises(NotImplementedError, toOutputAttrs, L9['bogus'], {}) - - -class TestColorStrip(unittest.TestCase): - - def testConvertDeviceToOutputAttrs(self): - out = toOutputAttrs(DeviceClass(L9['ChauvetColorStrip']), {DeviceAttr(L9['color']): HexColor('#ff0000')}) - self.assertEqual({L9['mode']: 215, L9['red']: 255, L9['green']: 0, L9['blue']: 0}, out) - - -class TestDimmer(unittest.TestCase): - - def testConvert(self): - self.assertEqual({L9['level']: 127}, toOutputAttrs(DeviceClass(L9['SimpleDimmer']), {DeviceAttr(L9['brightness']): .5})) - - -class TestMini15(unittest.TestCase): - - def testConvertColor(self): - out = toOutputAttrs(DeviceClass(L9['Mini15']), {DeviceAttr(L9['color']): HexColor('#010203')}) - self.assertEqual(255, out[OutputAttr(L9['dimmer'])]) - self.assertEqual(1, out[OutputAttr(L9['red'])]) - self.assertEqual(2, out[OutputAttr(L9['green'])]) - self.assertEqual(3, out[OutputAttr(L9['blue'])]) - - def testConvertRotation(self): - out = toOutputAttrs(DeviceClass(L9['Mini15']), {DeviceAttr(L9['rx']): 90, DeviceAttr(L9['ry']): 45}) - self.assertEqual(42, out[OutputAttr(L9['xRotation'])]) - self.assertEqual(127, out[OutputAttr(L9['xFine'])]) - self.assertEqual(47, out[OutputAttr(L9['yRotation'])]) - self.assertEqual(207, out[OutputAttr(L9['yFine'])]) - self.assertEqual(0, out[OutputAttr(L9['rotationSpeed'])]) - - -DC = DeviceClass(L9['someDev']) - - -class TestResolve(unittest.TestCase): - - def testMaxes1Color(self): - # do not delete - this one catches a bug in the rgb_to_hex(...) lines - self.assertEqual(HexColor('#ff0300'), resolve(DC, DeviceAttr(L9['color']), [HexColor('#ff0300')])) - - def testMaxes2Colors(self): - self.assertEqual(HexColor('#ff0400'), resolve(DC, DeviceAttr(L9['color']), [HexColor('#ff0300'), HexColor('#000400')])) - - def testMaxes3Colors(self): - self.assertEqual(HexColor('#112233'), resolve(DC, DeviceAttr(L9['color']), [HexColor('#110000'), HexColor('#002200'), HexColor('#000033')])) diff -r 623836db99af -r 4556eebe5d73 light9/collector/dmx_controller_output.py --- a/light9/collector/dmx_controller_output.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,73 +0,0 @@ -####################################################### -# DMX Controller -# See -# Copyright (C) Jonathan Brogdon -# This program is published under a GPLv2 license -# -# This code implements a DMX controller with UI provided -# by LCDproc -# -####################################################### -from pyftdi import ftdi - -#FTDI device info -vendor = 0x0403 -product = 0x6001 - - -##################### -# DMX USB controller -##################### -class OpenDmxUsb(): - - def __init__(self): - self.baud_rate = 250000 - self.data_bits = 8 - self.stop_bits = 2 - self.parity = 'N' - self.flow_ctrl = '' - self.rts_state = False - self._init_dmx() - - #Initialize the controller - def _init_dmx(self): - self.ftdi = ftdi.Ftdi() - self.ftdi.open(vendor, product, 0) - self.ftdi.set_baudrate(self.baud_rate) - self.ftdi.set_line_property(self.data_bits, - self.stop_bits, - self.parity, - break_=False) - self.ftdi.set_flowctrl(self.flow_ctrl) - self.ftdi.purge_rx_buffer() - self.ftdi.purge_tx_buffer() - self.ftdi.set_rts(self.rts_state) - - #Send DMX data - def send_dmx(self, channelVals): - assert self.ftdi.write_data(channelVals) == 513 - # Need to generate two bits for break - self.ftdi.set_line_property(self.data_bits, - self.stop_bits, - self.parity, - break_=True) - self.ftdi.set_line_property(self.data_bits, - self.stop_bits, - self.parity, - break_=True) - self.ftdi.set_line_property(self.data_bits, - self.stop_bits, - self.parity, - break_=False) - - -if __name__ == "__main__": - dmxUsb = OpenDmxUsb() - - channelVals = bytearray([0] * 513) - channelVals[0] = 0 # dummy channel 0 - while (True): - for x in range(1, 468 + 1): - channelVals[x] = 255 - - dmxUsb.send_dmx(channelVals) diff -r 623836db99af -r 4556eebe5d73 light9/collector/output.py --- a/light9/collector/output.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,310 +0,0 @@ -import asyncio -import logging -import socket -import struct -import time -from typing import cast -from light9.newtypes import uriTail - -import usb.core -from rdflib import URIRef -from twisted.internet import reactor, task -from twisted.internet.interfaces import IReactorCore - -from light9.metrics import metrics - -log = logging.getLogger('output') -logAllDmx = logging.getLogger('output.allDmx') - - -class Output: - """ - send a binary buffer of values to some output device. Call update - as often as you want- the result will be sent as soon as possible, - and with repeats as needed to outlast hardware timeouts. - - This base class doesn't ever call _write. Subclasses below have - strategies for that. - """ - uri: URIRef - - def __init__(self, uri: URIRef): - self.uri = uri - - self._currentBuffer = b'' - - if log.isEnabledFor(logging.DEBUG): - self._lastLoggedMsg = '' - task.LoopingCall(self._periodicLog).start(1) - - def reconnect(self): - pass - - def shortId(self) -> str: - """short string to distinguish outputs""" - return uriTail(self.uri) - - def update(self, buf: bytes) -> None: - """caller asks for the output to be this buffer""" - self._currentBuffer = buf - - def _periodicLog(self): - msg = '%s: %s' % (self.shortId(), ' '.join(map(str, self._currentBuffer))) - if msg != self._lastLoggedMsg: - log.debug(msg) - self._lastLoggedMsg = msg - - def _write(self, buf: bytes) -> None: - """ - write buffer to output hardware (may be throttled if updates are - too fast, or repeated if they are too slow) - """ - pass - - def crash(self): - log.error('unrecoverable- exiting') - cast(IReactorCore, reactor).crash() - - -class DummyOutput(Output): - - def __init__(self, uri, **kw): - super().__init__(uri) - - def update(self, buf: bytes): - log.info(f'dummy update {list(map(int,buf[:80]))}') - - -class BackgroundLoopOutput(Output): - """Call _write forever at 20hz in background threads""" - - rate: float - - def __init__(self, uri, rate=22): - super().__init__(uri) - self.rate = rate - self._currentBuffer = b'' - - self._task = asyncio.create_task(self._loop()) - - async def _loop(self): - while True: - t1 = time.time() - self._loop_one() - remain = max(0, 1 / self.rate - (time.time() - t1)) - await asyncio.sleep(remain) - - def _loop_one(self): - start = time.time() - sendingBuffer = self._currentBuffer - #tenacity retry - self._write(sendingBuffer) - - -class FtdiDmx(BackgroundLoopOutput): - - def __init__(self, uri, lastDmxChannel, rate=22): - super().__init__(uri) - self.lastDmxChannel = lastDmxChannel - from .dmx_controller_output import OpenDmxUsb - self.dmx = OpenDmxUsb() - - def _write(self, buf): - with metrics('write', output=self.shortId()).time(): - if not buf: - logAllDmx.debug('%s: empty buf- no output', self.shortId()) - return - - # ok to truncate the last channels if they just went - # to 0? No it is not. DMX receivers don't add implicit - # zeros there. - buf = bytes([0]) + buf[:self.lastDmxChannel] - - if logAllDmx.isEnabledFor(logging.DEBUG): - # for testing fps, smooth fades, etc - logAllDmx.debug('%s: %s...' % (self.shortId(), ' '.join(map(str, buf[:32])))) - - self.dmx.send_dmx(buf) - - -class ArtnetDmx(BackgroundLoopOutput): - # adapted from https://github.com/spacemanspiff2007/PyArtNet/blob/master/pyartnet/artnet_node.py (gpl3) - def __init__(self, uri, host, port, rate): - """sends UDP messages to the given host/port""" - super().__init__(uri, rate) - packet = bytearray() - packet.extend(map(ord, "Art-Net")) - packet.append(0x00) # Null terminate Art-Net - packet.extend([0x00, 0x50]) # Opcode ArtDMX 0x5000 (Little endian) - packet.extend([0x00, 0x0e]) # Protocol version 14 - self.base_packet = packet - self.sequence_counter = 255 - self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - - def _write(self, buf): - with metrics('write', output=self.shortId()).time(): - if not buf: - logAllDmx.debug('%s: empty buf- no output', self.shortId()) - return - - if logAllDmx.isEnabledFor(logging.DEBUG): - # for testing fps, smooth fades, etc - logAllDmx.debug('%s: %s...' % (self.shortId(), ' '.join(map(str, buf[:32])))) - - if self.sequence_counter: - self.sequence_counter += 1 - if self.sequence_counter > 255: - self.sequence_counter = 1 - packet = self.base_packet[:] - packet.append(self.sequence_counter) # Sequence, - packet.append(0x00) # Physical - universe_nr = 0 - packet.append(universe_nr & 0xFF) # Universe LowByte - packet.append(universe_nr >> 8 & 0xFF) # Universe HighByte - - packet.extend(struct.pack('>h', len(buf))) # Pack the number of channels Big endian - packet.extend(buf) - - self._socket.sendto(packet, ('127.0.0.1', 6454)) - - -class Udmx(BackgroundLoopOutput): - """alternate lib: - - from PyDMXControl.controllers import uDMXController - u = uDMXController(autostart=False) - u._connect() - u._transmit([255, 0, 0, ... - """ - - def __init__(self, uri: URIRef, bus: int, address: int, lastDmxChannel: int, rate: float): - self.bus = bus - self.address = address - self.lastDmxChannel = lastDmxChannel - self.dev = None - super().__init__(uri, rate=rate) - - self.reconnect() - - def shortId(self) -> str: - return super().shortId() + f'_bus={self.bus}' - - def reconnect(self): - metrics('connected', output=self.shortId()).set(0) - from pyudmx import pyudmx - self.dev = pyudmx.uDMXDevice() - if not self.dev.open(bus=self.bus, address=self.address): - raise ValueError("dmx open failed") - log.info(f'opened {self.dev}') - metrics('connected', output=self.shortId()).set(1) - metrics('reconnections', output=self.shortId()).inc() - - #def update(self, buf:bytes): - # self._write(buf) - - #def _loop(self): - # pass - def _write(self, buf): - if not self.dev: - log.info('%s: trying to connect', self.shortId()) - raise ValueError() - - with metrics('write', output=self.shortId()).time(): - try: - if not buf: - logAllDmx.debug('%s: empty buf- no output', self.shortId()) - return - - # ok to truncate the last channels if they just went - # to 0? No it is not. DMX receivers don't add implicit - # zeros there. - buf = buf[:self.lastDmxChannel] - - if logAllDmx.isEnabledFor(logging.DEBUG): - # for testing fps, smooth fades, etc - logAllDmx.debug('%s: %s...' % (self.shortId(), ' '.join(map(str, buf[:32])))) - t1 = time.time() - sent = self.dev.send_multi_value(1, bytearray(buf)) - if sent != len(buf): - raise ValueError("incomplete send") - except ValueError: - self.reconnect() - raise - except usb.core.USBError as e: - # not in main thread - if e.errno == 75: - metrics('write_overflow', output=self.shortId()).inc() - return - - if e.errno == 5: # i/o err - metrics('write_io_error', output=self.shortId()).inc() - return - - if e.errno == 32: # pipe err - metrics('write_pipe_error', output=self.shortId()).inc() - return - - msg = 'usb: sending %s bytes to %r; error %r' % (len(buf), self.uri, e) - log.warn(msg) - - if e.errno == 13: # permissions - return self.crash() - - if e.errno == 19: # no such dev; usb hw restarted - self.reconnect() - return - - raise - dt = time.time() - t1 - if dt > 1/self.rate*1.5: - log.warning(f'usb stall- took {(dt*1000):.2f}ms') - - -''' -# the code used in 2018 and before -class UdmxOld(BackgroundLoopOutput): - - def __init__(self, uri, bus): - from light9.io.udmx import Udmx - self._dev = Udmx(bus) - - super().__init__(uri) - - def _write(self, buf: bytes): - try: - if not buf: - return - self.dev.SendDMX(buf) - - except usb.core.USBError as e: - # not in main thread - if e.errno != 75: - msg = 'usb: sending %s bytes to %r; error %r' % ( - len(buf), self.uri, e) - log.warn(msg) - raise - - -# out of date -class EnttecDmx(BackgroundLoopOutput): - stats = scales.collection('/output/enttecDmx', scales.PmfStat('write', recalcPeriod=1), - scales.PmfStat('update', recalcPeriod=1)) - - def __init__(self, uri, devicePath='/dev/dmx0', numChannels=80): - sys.path.append("dmx_usb_module") - from dmx import Dmx - self.dev = Dmx(devicePath) - super().__init__(uri) - - - @stats.update.time() - def update(self, values): - - # I was outputting on 76 and it was turning on the light at - # dmx75. So I added the 0 byte. No notes explaining the footer byte. - self.currentBuffer = '\x00' + ''.join(map(chr, values)) + "\x00" - - @stats.write.time() - def _write(self, buf): - self.dev.write(buf) -''' diff -r 623836db99af -r 4556eebe5d73 light9/collector/output_test.py --- a/light9/collector/output_test.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,7 +0,0 @@ -import unittest -from light9.namespaces import L9 -# from light9.collector.output import DmxOutput - - - - diff -r 623836db99af -r 4556eebe5d73 light9/collector/service.py --- a/light9/collector/service.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,161 +0,0 @@ -#!bin/python -""" -Collector receives device attrs from multiple senders, combines -them, and sends output attrs to hardware. The combining part has -custom code for some attributes. - -Input can be over http or zmq. -""" -import asyncio -import functools -import logging -import subprocess -import traceback -from typing import List - -from light9 import networking -from light9.collector.collector import Collector -from light9.collector.output import ArtnetDmx, DummyOutput, Output, Udmx # noqa -from light9.collector.weblisteners import UiListener, WebListeners -from light9.namespaces import L9 -from light9.run_local import log -from light9.zmqtransport import parseJsonMessage -from rdfdb.syncedgraph.syncedgraph import SyncedGraph -from starlette.applications import Starlette -from starlette.endpoints import WebSocketEndpoint -from starlette.requests import ClientDisconnect -from starlette.responses import Response -from starlette.routing import Route, WebSocketRoute -from starlette.types import Receive, Scope, Send -from starlette.websockets import WebSocket -from starlette_exporter import PrometheusMiddleware, handle_metrics - -import zmq -import zmq.asyncio - - -# this is the rate sent to usb -RATE = 20 - - -class Updates(WebSocketEndpoint, UiListener): - - def __init__(self, listeners, scope: Scope, receive: Receive, send: Send) -> None: - super().__init__(scope, receive, send) - self.listeners = listeners - - async def on_connect(self, websocket: WebSocket): - await websocket.accept() - log.info('socket connect %s', self.scope['client']) - self.websocket = websocket - self.listeners.addClient(self) - - async def sendMessage(self, msgText): - await self.websocket.send_text(msgText) - - # async def on_receive(self, websocket, data): - # json.loads(data) - - async def on_disconnect(self, websocket: WebSocket, close_code: int): - self.listeners.delClient(self) - - pass - - -async def PutAttrs(collector: Collector, request): - try: - body = await request.body() - except ClientDisconnect: - log.warning("PUT /attrs request disconnected- ignoring") - return Response('', status_code=400) - client, clientSession, settings, sendTime = parseJsonMessage(collector.graph, body) - collector.setAttrs(client, clientSession, settings, sendTime) - return Response('', status_code=202) - - -async def zmqListener(collector): - try: - ctx = zmq.asyncio.Context() - sock = ctx.socket(zmq.SUB) - sock.bind('tcp://127.0.0.1:9203') - sock.subscribe(b'setAttr') - while True: - [topic, msg] = await sock.recv_multipart() - if topic != b'setAttr': - raise ValueError(topic) - # log.info(f'zmq recv {len(msg)}') - client, clientSession, settings, sendTime = parseJsonMessage(collector.graph, msg) - collector.setAttrs(client, clientSession, settings, sendTime) - except: - traceback.print_exc() - raise - -def findDevice(): - for line in subprocess.check_output("lsusb").decode('utf8').splitlines(): - if '16c0:05dc' in line: - words = line.split(':')[0].split() - dev = f'/dev/bus/usb/{words[1]}/{words[3]}' - log.info(f'device will be {dev}') - return dev ,int(words[3]) - raise ValueError("no matching uDMX found") - -def main(): - logging.getLogger('autodepgraphapi').setLevel(logging.INFO) - logging.getLogger('syncedgraph').setLevel(logging.INFO) - logging.getLogger('output.allDmx').setLevel(logging.WARNING) - logging.getLogger().setLevel(logging.DEBUG) - logging.getLogger('collector').setLevel(logging.DEBUG) - - graph = SyncedGraph(networking.rdfdb.url, "collector") - - devPath, usbAddress = findDevice() - # if user doesn't have r/w, fail now - try: - # todo: drive outputs with config files - outputs: List[Output] = [ - # ArtnetDmx(L9['output/dmxA/'], - # host='127.0.0.1', - # port=6445, - # rate=rate), - #sudo chmod a+rw /dev/bus/usb/003/021 - Udmx(L9['output/dmxA/'], bus=1, address=usbAddress, lastDmxChannel=200, rate=RATE), - ] - except Exception: - log.error("setting up outputs:") - traceback.print_exc() - raise - listeners = WebListeners() - c = Collector(graph, outputs, listeners) - zl = asyncio.create_task(zmqListener(c)) - app = Starlette( - debug=True, - routes=[ - # Route('/recentRequests', lambda req: get_recentRequests(req, db)), - WebSocketRoute('/updates', endpoint=functools.partial(Updates, listeners)), - Route('/attrs', functools.partial(PutAttrs, c), methods=['PUT']), - ], - ) - app.add_middleware(PrometheusMiddleware) - app.add_route("/metrics", handle_metrics) - - # loadtest = os.environ.get('LOADTEST', False) # call myself with some synthetic load then exit - # if loadtest: - # # in a subprocess since we don't want this client to be - # # cooperating with the main event loop and only sending - # # requests when there's free time - # def afterWarmup(): - # log.info('running collector_loadtest') - # d = utils.getProcessValue('bin/python', ['bin/collector_loadtest.py']) - - # def done(*a): - # log.info('loadtest done') - # reactor.stop() - - # d.addCallback(done) - - # reactor.callLater(2, afterWarmup) - - return app - - -app = main() diff -r 623836db99af -r 4556eebe5d73 light9/collector/weblisteners.py --- a/light9/collector/weblisteners.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,90 +0,0 @@ -import asyncio -import json -import logging -import time -from typing import Any, Awaitable, Dict, List, Protocol, Tuple - -from light9.collector.output import Output as OutputInstance -from light9.newtypes import (DeviceUri, DmxIndex, DmxMessageIndex, OutputAttr, OutputUri, OutputValue) - -log = logging.getLogger('weblisteners') - - -def shortenOutput(out: OutputUri) -> str: - return str(out).rstrip('/').rsplit('/', 1)[-1] - - -class UiListener(Protocol): - - async def sendMessage(self, msg): - ... - - -class WebListeners: - - def __init__(self) -> None: - self.clients: List[Tuple[UiListener, Dict[DeviceUri, Dict[OutputAttr, OutputValue]]]] = [] - self.pendingMessageForDev: Dict[DeviceUri, Tuple[Dict[OutputAttr, OutputValue], Dict[Tuple[DeviceUri, OutputAttr], Tuple[OutputUri, - DmxMessageIndex]]]] = {} - self.lastFlush = 0 - asyncio.create_task(self.flusher()) - - def addClient(self, client: UiListener): - self.clients.append((client, {})) # seen = {dev: attrs} - log.info('added client %s %s', len(self.clients), client) - # todo: it would be nice to immediately fill in the client on the - # latest settings, but I lost them so I can't. - - def delClient(self, client: UiListener): - self.clients = [(c, t) for c, t in self.clients if c != client] - log.info('delClient %s, %s left', client, len(self.clients)) - - def outputAttrsSet(self, dev: DeviceUri, attrs: Dict[OutputAttr, Any], outputMap: Dict[Tuple[DeviceUri, OutputAttr], Tuple[OutputUri, DmxMessageIndex]]): - """called often- don't be slow""" - self.pendingMessageForDev[dev] = (attrs, outputMap) - # maybe put on a stack for flusher or something - - async def flusher(self): - await asyncio.sleep(3) # help startup faster? - while True: - await self._flush() - await asyncio.sleep(.05) - - async def _flush(self): - now = time.time() - if now < self.lastFlush + .05 or not self.clients: - return - self.lastFlush = now - - while self.pendingMessageForDev: - dev, (attrs, outputMap) = self.pendingMessageForDev.popitem() - - msg = None # lazy, since makeMsg is slow - - sendAwaits: List[Awaitable[None]] = [] - - # this omits repeats, but can still send many - # messages/sec. Not sure if piling up messages for the browser - # could lead to slowdowns in the real dmx output. - for client, seen in self.clients: - if seen.get(dev) == attrs: - continue - if msg is None: - msg = self.makeMsg(dev, attrs, outputMap) - - seen[dev] = attrs - sendAwaits.append(client.sendMessage(msg)) - await asyncio.gather(*sendAwaits) - - def makeMsg(self, dev: DeviceUri, attrs: Dict[OutputAttr, Any], outputMap: Dict[Tuple[DeviceUri, OutputAttr], Tuple[OutputUri, DmxMessageIndex]]): - attrRows = [] - for attr, val in attrs.items(): - outputUri, bufIndex = outputMap[(dev, attr)] - dmxIndex = DmxIndex(bufIndex + 1) - attrRows.append({'attr': attr.rsplit('/')[-1], 'val': val, 'chan': (shortenOutput(outputUri), dmxIndex)}) - attrRows.sort(key=lambda r: r['chan']) - for row in attrRows: - row['chan'] = '%s %s' % (row['chan'][0], row['chan'][1]) - - msg = json.dumps({'outputAttrsSet': {'dev': dev, 'attrs': attrRows}}, sort_keys=True) - return msg diff -r 623836db99af -r 4556eebe5d73 light9/cursor1.xbm --- a/light9/cursor1.xbm Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,11 +0,0 @@ -#define cursor1_width 20 -#define cursor1_height 20 -#define cursor1_x_hot 5 -#define cursor1_y_hot 5 -static char cursor1_bits[] = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x07, 0x00, - 0x00, 0x1d, 0x00, 0x00, 0x27, 0x00, 0x00, 0x23, 0x00, 0x80, 0x21, 0x00, - 0x80, 0x21, 0x00, 0x80, 0x23, 0x00, 0x80, 0x3e, 0x00, 0x80, 0x1f, 0x00, - 0x80, 0x71, 0x00, 0x80, 0x47, 0x00, 0x80, 0x7c, 0x00, 0xc0, 0x00, 0x00, - 0x40, 0x00, 0x00, 0x20, 0x00, 0x00, 0x20, 0x00, 0x00, 0x10, 0x00, 0x00, - }; diff -r 623836db99af -r 4556eebe5d73 light9/curvecalc/__init__.py diff -r 623836db99af -r 4556eebe5d73 light9/curvecalc/client.py --- a/light9/curvecalc/client.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,25 +0,0 @@ -""" -client code for talking to curvecalc -""" -import cyclone.httpclient -from light9 import networking -import urllib.request, urllib.parse, urllib.error - - -def sendLiveInputPoint(curve, value): - f = cyclone.httpclient.fetch(networking.curveCalc.path('liveInputPoint'), - method='POST', - timeout=1, - postdata=urllib.parse.urlencode({ - 'curve': - curve, - 'value': - str(value), - })) - - @f.addCallback - def cb(result): - if result.code // 100 != 2: - raise ValueError("curveCalc said %s: %s", result.code, result.body) - - return f diff -r 623836db99af -r 4556eebe5d73 light9/curvecalc/cursors.py --- a/light9/curvecalc/cursors.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,22 +0,0 @@ -import logging -log = logging.getLogger("cursors") - -# accept ascii images, read file images, add hotspots, read xbm as -# cursor with @filename form - -_pushed = {} # widget : [old, .., newest] - - -def push(widget, new_cursor): - global _pushed - _pushed.setdefault(widget, []).append(widget.cget("cursor")) - - -def pop(widget): - global _pushed - try: - c = _pushed[widget].pop(-1) - except IndexError: - log.debug("cursor pop from empty stack") - return - widget.config(cursor=c) diff -r 623836db99af -r 4556eebe5d73 light9/curvecalc/curve.py --- a/light9/curvecalc/curve.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,384 +0,0 @@ -import logging, ast, os -from bisect import bisect_left, bisect -import louie as dispatcher -from twisted.internet import reactor -from rdflib import Literal -from light9 import showconfig -from light9.namespaces import L9, RDF, RDFS -from rdfdb.patch import Patch - -log = logging.getLogger() -# todo: move to config, consolidate with ascoltami, musicPad, etc -introPad = 4 -postPad = 4 - - -class Curve(object): - """curve does not know its name. see Curveset""" - - def __init__(self, uri, pointsStorage='graph'): - self.uri = uri - self.pointsStorage = pointsStorage - self.points = [] # x-sorted list of (x,y) - self._muted = False - - def __repr__(self): - return "<%s %s (%s points)>" % (self.__class__.__name__, self.uri, - len(self.points)) - - def muted(): - doc = "Whether to currently send levels (boolean, obviously)" - - def fget(self): - return self._muted - - def fset(self, val): - self._muted = val - dispatcher.send('mute changed', sender=self) - - return locals() - - muted = property(**muted()) - - def toggleMute(self): - self.muted = not self.muted - - def load(self, filename): - self.points[:] = [] - for line in open(filename): - x, y = line.split() - self.points.append((float(x), ast.literal_eval(y))) - self.points.sort() - dispatcher.send("points changed", sender=self) - - def set_from_string(self, pts): - self.points[:] = [] - vals = pts.split() - pairs = list(zip(vals[0::2], vals[1::2])) - for x, y in pairs: - self.points.append((float(x), ast.literal_eval(y))) - self.points.sort() - dispatcher.send("points changed", sender=self) - - def points_as_string(self): - - def outVal(x): - if isinstance(x, str): # markers - return x - return "%.4g" % x - - return ' '.join( - "%s %s" % (outVal(p[0]), outVal(p[1])) for p in self.points) - - def save(self, filename): - # this is just around for markers, now - if filename.endswith('-music') or filename.endswith('_music'): - print("not saving music track") - return - f = open(filename, 'w') - for p in self.points: - f.write("%s %r\n" % p) - f.close() - - def eval(self, t, allow_muting=True): - if self.muted and allow_muting: - return 0 - if not self.points: - raise ValueError("curve has no points") - i = bisect_left(self.points, (t, None)) - 1 - - if i == -1: - return self.points[0][1] - if self.points[i][0] > t: - return self.points[i][1] - if i >= len(self.points) - 1: - return self.points[i][1] - - p1, p2 = self.points[i], self.points[i + 1] - frac = (t - p1[0]) / (p2[0] - p1[0]) - y = p1[1] + (p2[1] - p1[1]) * frac - return y - - __call__ = eval - - def insert_pt(self, new_pt): - """returns index of new point""" - i = bisect(self.points, (new_pt[0], None)) - self.points.insert(i, new_pt) - # missing a check that this isn't the same X as the neighbor point - dispatcher.send("points changed", sender=self) - return i - - def live_input_point(self, new_pt, clear_ahead_secs=.01): - x, y = new_pt - exist = self.points_between(x, x + clear_ahead_secs) - for pt in exist: - self.remove_point(pt) - self.insert_pt(new_pt) - dispatcher.send("points changed", sender=self) - # now simplify to the left - - def set_points(self, updates): - for i, pt in updates: - self.points[i] = pt - - # this should be on, but live_input_point made it fail a - # lot. need a new solution. - #self.checkOverlap() - dispatcher.send("points changed", sender=self) - - def checkOverlap(self): - x = None - for p in self.points: - if p[0] <= x: - raise ValueError("overlapping points") - x = p[0] - - def pop_point(self, i): - p = self.points.pop(i) - dispatcher.send("points changed", sender=self) - return p - - def remove_point(self, pt): - self.points.remove(pt) - dispatcher.send("points changed", sender=self) - - def indices_between(self, x1, x2, beyond=0): - leftidx = max(0, bisect(self.points, (x1, None)) - beyond) - rightidx = min(len(self.points), - bisect(self.points, (x2, None)) + beyond) - return list(range(leftidx, rightidx)) - - def points_between(self, x1, x2): - """returns (x,y) points""" - return [self.points[i] for i in self.indices_between(x1, x2)] - - def point_before(self, x): - """(x,y) of the point left of x, or None""" - leftidx = self.index_before(x) - if leftidx is None: - return None - return self.points[leftidx] - - def index_before(self, x): - leftidx = bisect(self.points, (x, None)) - 1 - if leftidx < 0: - return None - return leftidx - - -class CurveResource(object): - """ - holds a Curve, deals with graphs - """ - - def __init__(self, graph, uri): - # probably newCurve and loadCurve should be the constructors instead. - self.graph, self.uri = graph, uri - - def curvePointsContext(self): - return self.uri - - def newCurve(self, ctx, label): - """ - Save type/label for a new :Curve resource. - Pass the ctx where the main curve data (not the points) will go. - """ - if hasattr(self, 'curve'): - raise ValueError('CurveResource already has a curve %r' % - self.curve) - self.graph.patch( - Patch(addQuads=[ - (self.uri, RDF.type, L9['Curve'], ctx), - (self.uri, RDFS.label, label, ctx), - ])) - self.curve = Curve(self.uri) - self.curve.points.extend([(0, 0)]) - self.saveCurve() - self.watchCurvePointChanges() - - def loadCurve(self): - if hasattr(self, 'curve'): - raise ValueError('CurveResource already has a curve %r' % - self.curve) - pointsFile = self.graph.value(self.uri, L9['pointsFile']) - self.curve = Curve(self.uri, - pointsStorage='file' if pointsFile else 'graph') - if hasattr(self.graph, 'addHandler'): - self.graph.addHandler(self.pointsFromGraph) - else: - # given a currentState graph - self.pointsFromGraph() - - def pointsFromGraph(self): - pts = self.graph.value(self.uri, L9['points']) - if pts is not None: - self.curve.set_from_string(pts) - else: - diskPts = self.graph.value(self.uri, L9['pointsFile']) - if diskPts is not None: - self.curve.load(os.path.join(showconfig.curvesDir(), diskPts)) - else: - log.warn("curve %s has no points", self.uri) - self.watchCurvePointChanges() - - def saveCurve(self): - self.pendingSave = None - for p in self.getSavePatches(): - self.graph.patch(p) - - def getSavePatches(self): - if self.curve.pointsStorage == 'file': - log.warn("not saving file curves anymore- skipping %s" % self.uri) - #cur.save("%s-%s" % (basename,name)) - return [] - elif self.curve.pointsStorage == 'graph': - return [ - self.graph.getObjectPatch(self.curvePointsContext(), - subject=self.uri, - predicate=L9['points'], - newObject=Literal( - self.curve.points_as_string())) - ] - else: - raise NotImplementedError(self.curve.pointsStorage) - - def watchCurvePointChanges(self): - """start watching and saving changes to the graph""" - dispatcher.connect(self.onChange, 'points changed', sender=self.curve) - - def onChange(self): - - # Don't write a patch for the edited curve points until they've been - # stable for this long. This can be very short, since it's just to - # stop a 100-point edit from sending many updates. If it's too long, - # you won't see output lights change while you drag a point. Todo: - # this is just the wrong timing algorithm- it should be a max rate, - # not a max-hold-still-time. - HOLD_POINTS_GRAPH_COMMIT_SECS = .1 - - if getattr(self, 'pendingSave', None): - self.pendingSave.cancel() - self.pendingSave = reactor.callLater(HOLD_POINTS_GRAPH_COMMIT_SECS, - self.saveCurve) - - -class Markers(Curve): - """Marker is like a point but the y value is a string""" - - def eval(self): - raise NotImplementedError() - - -def slope(p1, p2): - if p2[0] == p1[0]: - return 0 - return (p2[1] - p1[1]) / (p2[0] - p1[0]) - - -class Curveset(object): - - def __init__(self, graph, session): - self.graph, self.session = graph, session - - self.currentSong = None - self.curveResources = {} # uri : CurveResource - - self.markers = Markers(uri=None, pointsStorage='file') - - graph.addHandler(self.loadCurvesForSong) - - def curveFromUri(self, uri): - return self.curveResources[uri].curve - - def loadCurvesForSong(self): - """ - current curves will track song's curves. - - This fires 'add_curve' dispatcher events to announce the new curves. - """ - log.info('loadCurvesForSong') - dispatcher.send("clear_curves") - self.curveResources.clear() - self.markers = Markers(uri=None, pointsStorage='file') - - self.currentSong = self.graph.value(self.session, L9['currentSong']) - if self.currentSong is None: - return - - for uri in sorted(self.graph.objects(self.currentSong, L9['curve'])): - try: - cr = self.curveResources[uri] = CurveResource(self.graph, uri) - cr.loadCurve() - - curvename = self.graph.label(uri) - if not curvename: - raise ValueError("curve %r has no label" % uri) - dispatcher.send("add_curve", - sender=self, - uri=uri, - label=curvename, - curve=cr.curve) - except Exception as e: - log.error("loading %s failed: %s", uri, e) - - basename = os.path.join( - showconfig.curvesDir(), - showconfig.songFilenameFromURI(self.currentSong)) - try: - self.markers.load("%s.markers" % basename) - except IOError: - print("no marker file found") - - def save(self): - """writes a file for each curve with a name - like basename-curvename, or saves them to the rdf graph""" - basename = os.path.join( - showconfig.curvesDir(), - showconfig.songFilenameFromURI(self.currentSong)) - - patches = [] - for cr in list(self.curveResources.values()): - patches.extend(cr.getSavePatches()) - - self.markers.save("%s.markers" % basename) - # this will cause reloads that will rebuild our curve list - for p in patches: - self.graph.patch(p) - - def sorter(self, name): - return self.curves[name].uri - - def curveUrisInOrder(self): - return sorted(self.curveResources.keys()) - - def currentCurves(self): - # deprecated - for uri, cr in sorted(self.curveResources.items()): - with self.graph.currentState(tripleFilter=(uri, RDFS['label'], - None)) as g: - yield uri, g.label(uri), cr.curve - - def globalsdict(self): - raise NotImplementedError('subterm used to get a dict of name:curve') - - def get_time_range(self): - return 0, dispatcher.send("get max time")[0][1] - - def new_curve(self, name): - if isinstance(name, Literal): - name = str(name) - - uri = self.graph.sequentialUri(self.currentSong + '/curve-') - - cr = self.curveResources[uri] = CurveResource(self.graph, uri) - cr.newCurve(ctx=self.currentSong, label=Literal(name)) - s, e = self.get_time_range() - cr.curve.points.extend([(s, 0), (e, 0)]) - - ctx = self.currentSong - self.graph.patch( - Patch(addQuads=[ - (self.currentSong, L9['curve'], uri, ctx), - ])) - cr.saveCurve() diff -r 623836db99af -r 4556eebe5d73 light9/curvecalc/curvecalc.glade --- a/light9/curvecalc/curvecalc.glade Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1042 +0,0 @@ - - - - - - - 100 - 1 - 10 - - - Mousewheel zoom; C-p play/pause music at mouse -Keys in a selected curve: 1..5 add point at time cursor -Keys in any curve: q,w,e,r,t,y set marker at time cursor -Curve point bindings: B1 drag point; C-B1 curve add point; S-B1 sketch points; B1 drag select points - -Old subterm system may still work: -Drag sub into curve area for new curve+subterm -Available in functions: nsin/ncos period=amp=1; within(a,b) bef(x) aft(x) compare to time; smoove(x) cubic smoothstep; chan(name); curvename(t) eval curve - - - False - - - True - False - vertical - - - True - False - - - False - True - False - _Curvecalc - True - - - True - False - - - gtk-save - False - True - False - True - True - - - - - - - gtk-quit - False - True - False - True - True - - - - - - - - - - - False - True - False - _Edit - True - - - True - False - - - gtk-cut - False - True - False - True - True - - - - - gtk-copy - False - True - False - True - True - - - - - gtk-paste - False - True - False - True - True - - - - - gtk-delete - False - True - False - True - True - - - - - - - - - - - False - True - False - _Create - True - - - True - False - - - False - True - False - Curve... - True - - - - - - - False - True - False - Subterm... - True - - - - - - - - - - False - True - False - _View - True - - - True - False - - - False - True - False - See current time - True - - - - - - - False - True - False - See from current time -> end - True - - - - - - - False - True - False - Zoom all - True - - - - - - - False - True - False - Zoom in (wheel up) - True - - - - - False - True - False - Zoom out (wheel down) - True - - - - - False - True - False - Redraw curves - True - - - - - - - - - - False - True - False - _Playback - True - - - True - False - - - False - True - False - _Play/pause - True - - - - - - - - - - - False - True - False - Poin_ts - True - - - True - False - - - False - True - False - Delete - True - - - - - - - - - False - True - False - Debug - True - - - True - False - - - False - True - False - Python console - True - - - - - - - - - - - False - True - 0 - - - - - True - False - - - True - False - - - - - - False - True - 0 - - - - - True - False - 1 - Player is on song - right - - - False - False - 1 - - - - - (song) - False - True - True - True - True - none - 0 - http://glade.gnome.org - - - False - False - 2 - - - - - follow player song choice - False - True - True - False - 0.5 - True - - - False - False - 15 - 3 - - - - - - - - False - False - 1 - - - - - 600 - True - True - vertical - 600 - - - 400 - True - True - True - - - 100 - True - False - vertical - - - True - False - 7 - - - gtk-add - False - True - True - True - True - - - - False - False - 0 - - - - - - - - - - - - - - - - - - - - False - False - 0 - - - - - True - False - vertical - - - True - False - [zoom control] - - - False - True - 0 - - - - - False - True - 1 - - - - - True - True - never - - - True - False - - - True - True - - - True - False - vertical - - - - - - - - - - - - - - - - - - True - True - 2 - - - - - - - True - False - Curves - - - - - True - False - - - - - True - True - - - True - False - vertical - - - True - True - adjustment1 - never - - - True - False - adjustment1 - none - - - True - False - 2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - True - 0 - - - - - True - False - 18 - Drop new sub here - - - - False - True - 1 - - - - - True - False - - - - - - gtk-add - False - True - True - True - True - - - - False - False - 1 - - - - - - - - False - False - 2 - - - - - - - True - False - Subterms - - - - - False - False - - - - - True - True - 2 - - - - - True - False - - - True - False - 0 - none - - - True - False - 2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - False - Status - True - - - - - True - True - 0 - - - - - True - True - False - word - help - - - True - True - 1 - - - - - False - False - 5 - 3 - - - - - - - True - False - gtk-refresh - - - - False - 5 - popup - New curve - True - mouse - normal - - - True - False - 2 - - - True - False - end - - - gtk-cancel - False - True - True - True - True - - - False - False - 0 - - - - - gtk-add - False - True - True - True - True - True - True - - - False - False - 1 - - - - - False - True - end - 0 - - - - - True - False - Name for new subterm - - - True - True - 1 - - - - - True - False - vertical - - - True - True - True - True - liststore1 - True - 0 - - - False - - - - - True - True - 0 - - - - - _Make new curve with the same name - False - True - True - False - True - 0.5 - True - True - - - True - True - 1 - - - - - True - True - 2 - - - - - - button12 - button3 - - - - False - 5 - New curve - True - mouse - normal - - - True - False - 2 - - - True - False - end - - - gtk-cancel - False - True - True - True - True - - - False - False - 0 - - - - - gtk-add - False - True - True - True - True - True - True - - - False - False - 1 - - - - - False - True - end - 0 - - - - - True - False - Name for new curve - - - True - True - 1 - - - - - True - True - True - True - - True - False - False - - - True - True - 2 - - - - - - button5 - button4 - - - - - - song01(t) - - - True - False - vertical - - - 289 - 120 - True - False - gtk-missing-image - - - False - False - 0 - - - - - True - False - 0.4699999988079071 - vidref from Sat 15:30 - - - True - True - 1 - - - - diff -r 623836db99af -r 4556eebe5d73 light9/curvecalc/curveedit.py --- a/light9/curvecalc/curveedit.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,55 +0,0 @@ -""" -this may be split out from curvecalc someday, since it doesn't -need to be tied to a gui """ -import cgi - -from louie import dispatcher -from rdflib import URIRef -from twisted.internet import reactor -import cyclone.web - -from cycloneerr import PrettyErrorHandler - - -def serveCurveEdit(port, hoverTimeResponse, curveset): - """ - /hoverTime requests actually are handled by the curvecalc gui - """ - curveEdit = CurveEdit(curveset) - - class HoverTime(PrettyErrorHandler, cyclone.web.RequestHandler): - - def get(self): - hoverTimeResponse(self) - - class LiveInputPoint(PrettyErrorHandler, cyclone.web.RequestHandler): - - def post(self): - params = cgi.parse_qs(self.request.body) - curve = URIRef(params['curve'][0]) - value = float(params['value'][0]) - curveEdit.liveInputPoint(curve, value) - self.set_status(204) - - reactor.listenTCP( - port, - cyclone.web.Application(handlers=[ - (r'/hoverTime', HoverTime), - (r'/liveInputPoint', LiveInputPoint), - ], - debug=True)) - - -class CurveEdit(object): - - def __init__(self, curveset): - self.curveset = curveset - dispatcher.connect(self.inputTime, "input time") - self.currentTime = 0 - - def inputTime(self, val): - self.currentTime = val - - def liveInputPoint(self, curveUri, value): - curve = self.curveset.curveFromUri(curveUri) - curve.live_input_point((self.currentTime, value), clear_ahead_secs=.5) diff -r 623836db99af -r 4556eebe5d73 light9/curvecalc/curveview.py --- a/light9/curvecalc/curveview.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1360 +0,0 @@ -import math, logging -from gi.repository import Gtk -from gi.repository import Gdk -from gi.repository import GooCanvas -import louie as dispatcher -from rdflib import Literal -from twisted.internet import reactor -from light9.curvecalc.zoomcontrol import RegionZoom -from light9.curvecalc.curve import introPad, postPad -from lib.goocanvas_compat import Points, polyline_new_line -import imp - -log = logging.getLogger() -print("curveview.py toplevel") - - -def vlen(v): - return math.sqrt(v[0] * v[0] + v[1] * v[1]) - - -def angle_between(base, p0, p1): - p0 = p0[0] - base[0], p0[1] - base[1] - p1 = p1[0] - base[0], p1[1] - base[1] - p0 = [x / vlen(p0) for x in p0] - p1 = [x / vlen(p1) for x in p1] - dot = p0[0] * p1[0] + p0[1] * p1[1] - dot = max(-1, min(1, dot)) - return math.degrees(math.acos(dot)) - - -class Sketch: - """a sketch motion on a curveview, with temporary points while you - draw, and simplification when you release""" - - def __init__(self, curveview, ev): - self.curveview = curveview - self.pts = [] - self.last_x = None - - def motion(self, ev): - p = self.curveview.world_from_screen(ev.x, ev.y) - p = p[0], max(0, min(1, p[1])) - if self.last_x is not None and abs(ev.x - self.last_x) < 4: - return - self.last_x = ev.x - self.pts.append(p) - self.curveview.add_point(p) - - def release(self, ev): - pts = sorted(self.pts) - finalPoints = pts[:] - - dx = .01 - to_remove = [] - for i in range(1, len(pts) - 1): - x = pts[i][0] - - p_left = (x - dx, self.curveview.curve(x - dx)) - p_right = (x + dx, self.curveview.curve(x + dx)) - - if angle_between(pts[i], p_left, p_right) > 160: - to_remove.append(i) - - for i in to_remove: - self.curveview.curve.remove_point(pts[i]) - finalPoints.remove(pts[i]) - - # the simplified curve may now be too far away from some of - # the points, so we'll put them back. this has an unfortunate - # bias toward reinserting the earlier points - for i in to_remove: - p = pts[i] - if abs(self.curveview.curve(p[0]) - p[1]) > .1: - self.curveview.add_point(p) - finalPoints.append(p) - - self.curveview.update_curve() - self.curveview.select_points(finalPoints) - - -class SelectManip(object): - """ - selection manipulator is created whenever you have a selection. It - draws itself on the canvas and edits the points when you drag - various parts - """ - - def __init__(self, parent, getSelectedIndices, getWorldPoint, - getScreenPoint, getCanvasHeight, setPoints, getWorldTime, - getWorldValue, getDragRange): - """parent goocanvas group""" - self.getSelectedIndices = getSelectedIndices - self.getWorldPoint = getWorldPoint - self.getScreenPoint = getScreenPoint - self.getCanvasHeight = getCanvasHeight - self.setPoints = setPoints - self.getWorldTime = getWorldTime - self.getDragRange = getDragRange - self.getWorldValue = getWorldValue - self.grp = GooCanvas.CanvasGroup(parent=parent) - - self.title = GooCanvas.CanvasText(parent=self.grp, - text="selectmanip", - x=10, - y=10, - fill_color='white', - font="ubuntu 10") - - self.bbox = GooCanvas.CanvasRect(parent=self.grp, - fill_color_rgba=0xffff0030, - line_width=0) - - self.xTrans = polyline_new_line( - parent=self.grp, - close_path=True, - fill_color_rgba=0xffffff88, - ) - self.centerScale = polyline_new_line( - parent=self.grp, - close_path=True, - fill_color_rgba=0xffffff88, - ) - - thickLine = lambda: polyline_new_line( - parent=self.grp, stroke_color_rgba=0xffffccff, line_width=6) - self.leftScale = thickLine() - self.rightScale = thickLine() - self.topScale = thickLine() - - for grp, name in [ - (self.xTrans, 'x'), - (self.leftScale, 'left'), - (self.rightScale, 'right'), - (self.topScale, 'top'), - (self.centerScale, 'centerScale'), - ]: - grp.connect("button-press-event", self.onPress, name) - grp.connect("button-release-event", self.onRelease, name) - grp.connect("motion-notify-event", self.onMotion, name) - grp.connect("enter-notify-event", self.onEnter, name) - grp.connect("leave-notify-event", self.onLeave, name) - # and hover highlight - self.update() - - def onEnter(self, item, target_item, event, param): - self.prevColor = item.props.stroke_color_rgba - item.props.stroke_color_rgba = 0xff0000ff - - def onLeave(self, item, target_item, event, param): - item.props.stroke_color_rgba = self.prevColor - - def onPress(self, item, target_item, event, param): - self.dragStartTime = self.getWorldTime(event.x) - idxs = self.getSelectedIndices() - - self.origPoints = [self.getWorldPoint(i) for i in idxs] - self.origMaxValue = max(p[1] for p in self.origPoints) - moveLeft, moveRight = self.getDragRange(idxs) - - if param == 'centerScale': - self.maxPointMove = min(moveLeft, moveRight) - - self.dragRange = (self.dragStartTime - moveLeft, - self.dragStartTime + moveRight) - return True - - def onMotion(self, item, target_item, event, param): - if hasattr(self, 'dragStartTime'): - origPts = list(zip(self.getSelectedIndices(), self.origPoints)) - left = origPts[0][1][0] - right = origPts[-1][1][0] - width = right - left - dontCross = .001 - - clampLo = left if param == 'right' else self.dragRange[0] - clampHi = right if param == 'left' else self.dragRange[1] - - def clamp(x, lo, hi): - return max(lo, min(hi, x)) - - mouseT = self.getWorldTime(event.x) - clampedT = clamp(mouseT, clampLo + dontCross, clampHi - dontCross) - - dt = clampedT - self.dragStartTime - - if param == 'x': - self.setPoints( - (i, (orig[0] + dt, orig[1])) for i, orig in origPts) - elif param == 'left': - self.setPoints( - (i, - (left + dt + (orig[0] - left) / width * - clamp(width - dt, dontCross, right - clampLo - dontCross), - orig[1])) for i, orig in origPts) - elif param == 'right': - self.setPoints( - (i, - (left + (orig[0] - left) / width * - clamp(width + dt, dontCross, clampHi - left - dontCross), - orig[1])) for i, orig in origPts) - elif param == 'top': - v = self.getWorldValue(event.y) - if self.origMaxValue == 0: - self.setPoints((i, (orig[0], v)) for i, orig in origPts) - else: - scl = max(0, - min(1 / self.origMaxValue, v / self.origMaxValue)) - self.setPoints( - (i, (orig[0], orig[1] * scl)) for i, orig in origPts) - - elif param == 'centerScale': - dt = mouseT - self.dragStartTime - rad = width / 2 - tMid = left + rad - maxScl = (rad + self.maxPointMove - dontCross) / rad - newWidth = max(dontCross / width, min( - (rad + dt) / rad, maxScl)) * width - self.setPoints( - (i, (tMid + ((orig[0] - left) / width - .5) * newWidth, - orig[1])) for i, orig in origPts) - - def onRelease(self, item, target_item, event, param): - if hasattr(self, 'dragStartTime'): - del self.dragStartTime - - def update(self): - """if the view or selection or selected point positions - change, call this to redo the layout of the manip""" - idxs = self.getSelectedIndices() - pts = [self.getScreenPoint(i) for i in idxs] - - b = self.bbox.props - b.x = min(p[0] for p in pts) - 5 - b.y = min(p[1] for p in pts) - 5 - margin = 10 if len(pts) > 1 else 0 - b.width = max(p[0] for p in pts) - b.x + margin - b.height = min( - max(p[1] for p in pts) - b.y + margin, - self.getCanvasHeight() - b.y - 1) - - multi = (GooCanvas.CanvasItemVisibility.VISIBLE - if len(pts) > 1 else GooCanvas.CanvasItemVisibility.INVISIBLE) - b.visibility = multi - self.leftScale.props.visibility = multi - self.rightScale.props.visibility = multi - self.topScale.props.visibility = multi - self.centerScale.props.visibility = multi - - self.title.props.text = "%s %s selected" % ( - len(idxs), "point" if len(idxs) == 1 else "points") - - centerX = b.x + b.width / 2 - - midY = self.getCanvasHeight() * .5 - loY = self.getCanvasHeight() * .8 - - self.leftScale.props.points = Points([(b.x, b.y), - (b.x, b.y + b.height)]) - self.rightScale.props.points = Points([(b.x + b.width, b.y), - (b.x + b.width, b.y + b.height)]) - - self.topScale.props.points = Points([(b.x, b.y), (b.x + b.width, b.y)]) - - self.updateXTrans(centerX, midY) - - self.centerScale.props.points = Points([(centerX - 5, loY - 5), - (centerX + 5, loY - 5), - (centerX + 5, loY + 5), - (centerX - 5, loY + 5)]) - - def updateXTrans(self, centerX, midY): - x1 = centerX - 30 - x2 = centerX - 20 - x3 = centerX + 20 - x4 = centerX + 30 - y1 = midY - 10 - y2 = midY - 5 - y3 = midY + 5 - y4 = midY + 10 - shape = [ - (x1, midY), # left tip - (x2, y1), - (x2, y2), - (x3, y2), - (x3, y1), - (x4, midY), # right tip - (x3, y4), - (x3, y3), - (x2, y3), - (x2, y4) - ] - - self.xTrans.props.points = Points(shape) - - def destroy(self): - self.grp.remove() - - -class Curveview(object): - """ - graphical curve widget only. Please pack .widget - - - -> self.widget - -> EventBox - -> Box vertical, for border - -> self.canvas GooCanvas - -> root CanvasItem - ..various groups and items.. - - The canvas x1/x2/y1/y2 coords are updated to match self.widget. - - """ - - def __init__(self, - curve, - markers, - knobEnabled=False, - isMusic=False, - zoomControl=None): - """knobEnabled=True highlights the previous key and ties it to a - hardware knob""" - self.curve = curve - self.markers = markers - self.knobEnabled = knobEnabled - self._isMusic = isMusic - self.zoomControl = zoomControl - - self.redrawsEnabled = False - - box = self.createOuterWidgets() - self.canvas = self.createCanvasWidget(box) - self.trackWidgetSize() - self.update_curve() - - self._time = -999 - self.last_mouse_world = None - self.entered = False # is the mouse currently over this widget - self.selected_points = [] # idx of points being dragged - self.dots = {} - # self.bind("",self.focus) - dispatcher.connect(self.playPause, "onPlayPause") - dispatcher.connect(self.input_time, "input time") - dispatcher.connect(self.update_curve, "zoom changed") - dispatcher.connect(self.update_curve, - "points changed", - sender=self.curve) - dispatcher.connect(self.update_curve, "mute changed", sender=self.curve) - dispatcher.connect(self.select_between, "select between") - dispatcher.connect(self.acls, "all curves lose selection") - if self.knobEnabled: - dispatcher.connect(self.knob_in, "knob in") - dispatcher.connect(self.slider_in, "set key") - - # todo: hold control to get a [+] cursor - # def curs(ev): - # print ev.state - # self.bind("",curs) - # self.bind("",lambda ev: curs(0)) - - # this binds on c-a-b1, etc - if 0: # unported - self.regionzoom = RegionZoom(self, self.world_from_screen, - self.screen_from_world) - - self.sketch = None # an in-progress sketch - - self.dragging_dots = False - self.selecting = False - - def acls(self, butNot=None): - if butNot is self: - return - self.unselect() - - def createOuterWidgets(self): - self.timelineLine = self.curveGroup = self.selectManip = None - self.widget = Gtk.EventBox() - self.widget.set_can_focus(True) - self.widget.add_events(Gdk.EventMask.KEY_PRESS_MASK | - Gdk.EventMask.FOCUS_CHANGE_MASK) - self.onFocusOut() - - box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) - box.set_border_width(1) - self.widget.add(box) - box.show() - return box - - def trackWidgetSize(self): - """ - Also tried: - - visibility-notify-event - (Gdk.EventMask.VISIBILITY_NOTIFY_MASK) fires on some - resizes but definitely not all. During window resizes, - sometimes I have to 'shake' the window size to get all - curves to update. - - configure-event seems to never fire. - - size-allocate seems right but i get into infinite bounces - between two sizes - """ - - def sizeEvent(w, alloc): - p = self.canvas.props - if (alloc.width, alloc.height) != (p.x2, p.y2): - p.x1, p.x2 = 0, alloc.width - p.y1, p.y2 = 0, alloc.height - # calling update_curve in this event usually doesn't work - reactor.callLater(0, self.update_curve) - return False - - #self.widget.connect('size-allocate', sizeEvent) # see docstring - - def visEvent(w, alloc): - self.setCanvasToWidgetSize() - return False - - self.widget.add_events(Gdk.EventMask.VISIBILITY_NOTIFY_MASK) - self.widget.connect('visibility-notify-event', visEvent) - - def setCanvasToWidgetSize(self): - p = self.canvas.props - w = self.widget.get_allocated_width() - h = self.widget.get_allocated_height() - if (w, h) != (p.x2, p.y2): - p.x1, p.x2 = 0, w - p.y1, p.y2 = 0, h - self.update_curve() - - def createCanvasWidget(self, parent): - # this is only separate from createOuterWidgets because in the - # past, i worked around display bugs by recreating the whole - # canvas widget. If that's not necessary, this could be more - # clearly combined with createOuterWidgets since there's no - # time you'd want that one but not this one. - canvas = GooCanvas.Canvas() - parent.pack_start(canvas, expand=True, fill=True, padding=0) - canvas.show() - - p = canvas.props - p.background_color = 'black' - root = canvas.get_root_item() - - canvas.connect("leave-notify-event", self.onLeave) - canvas.connect("enter-notify-event", self.onEnter) - canvas.connect("motion-notify-event", self.onMotion) - canvas.connect("scroll-event", self.onScroll) - canvas.connect("button-release-event", self.onRelease) - root.connect("button-press-event", self.onCanvasPress) - - self.widget.connect("key-press-event", self.onKeyPress) - - self.widget.connect("focus-in-event", self.onFocusIn) - self.widget.connect("focus-out-event", self.onFocusOut) - #self.widget.connect("event", self.onAny) - return canvas - - def onAny(self, w, event): - print(" %s on %s" % (event, w)) - - def onFocusIn(self, *args): - dispatcher.send('curve row focus change') - dispatcher.send("all curves lose selection", butNot=self) - - self.widget.modify_bg(Gtk.StateFlags.NORMAL, Gdk.color_parse("red")) - - def onFocusOut(self, widget=None, event=None): - dispatcher.send('curve row focus change') - self.widget.modify_bg(Gtk.StateFlags.NORMAL, Gdk.color_parse("gray30")) - - # you'd think i'm unselecting when we lose focus, but we also - # lose focus when the user moves off the toplevel window, and - # that's not a time to forget the selection. See the 'all - # curves lose selection' signal for the fix. - - def onKeyPress(self, widget, event): - if event.string in list('12345'): - x = int(event.string) - self.add_point((self.current_time(), (x - 1) / 4.0)) - if event.string in list('qwerty'): - self.add_marker((self.current_time(), event.string)) - - def onDelete(self): - if self.selected_points: - self.remove_point_idx(*self.selected_points) - - def onCanvasPress(self, item, target_item, event): - # when we support multiple curves per canvas, this should find - # the close one and add a point to that. Binding to the line - # itself is probably too hard to hit. Maybe a background-color - # really thick line would be a nice way to allow a sloppier - # click - - self.widget.grab_focus() - - _, flags = event.get_state() - if flags & Gdk.ModifierType.CONTROL_MASK: - self.new_point_at_mouse(event) - elif flags & Gdk.ModifierType.SHIFT_MASK: - self.sketch_press(event) - else: - self.select_press(event) - - # this stops some other handler that wants to unfocus - return True - - def playPause(self): - """ - user has pressed ctrl-p over a curve view, possibly this - one. Returns the time under the mouse if we know it, or else - None - - todo: there should be a faint timecursor line under the mouse - so it's more obvious that we use that time for some - events. Rt-click should include Ctrl+P as 'play/pause from - here' - """ - # maybe self.canvas.get_pointer would be ok for this? i didn't try it - if self.entered and hasattr(self, 'lastMouseX'): - t = self.world_from_screen(self.lastMouseX, 0)[0] - return t - return None - - def goLive(self): - """this is for startup performance only, since the curves were - getting redrawn many times. """ - self.redrawsEnabled = True - self.update_curve() - - def knob_in(self, curve, value): - """user turned a hardware knob, which edits the point to the - left of the current time""" - if curve != self.curve: - return - idx = self.curve.index_before(self.current_time()) - if idx is not None: - pos = self.curve.points[idx] - self.curve.set_points([(idx, (pos[0], value))]) - - def slider_in(self, curve, value=None): - """user pushed on a slider. make a new key. if value is None, - the value will be the same as the last.""" - if curve != self.curve: - return - - if value is None: - value = self.curve.eval(self.current_time()) - - self.curve.insert_pt((self.current_time(), value)) - - def print_state(self, msg=""): - if 0: - print("%s: dragging_dots=%s selecting=%s" % - (msg, self.dragging_dots, self.selecting)) - - def select_points(self, pts): - """set selection to the given point values (tuples, not indices)""" - idxs = [] - for p in pts: - idxs.append(self.curve.points.index(p)) - self.select_indices(idxs) - - def select_indices(self, idxs): - """set selection to these point indices. This is the only - writer to self.selected_points""" - self.selected_points = idxs - self.highlight_selected_dots() - if self.selected_points and not self.selectManip: - self.selectManip = SelectManip( - self.canvas.get_root_item(), - getSelectedIndices=lambda: sorted(self.selected_points), - getWorldPoint=lambda i: self.curve.points[i], - getScreenPoint=lambda i: self.screen_from_world(self.curve. - points[i]), - getWorldTime=lambda x: self.world_from_screen(x, 0)[0], - getWorldValue=lambda y: self.world_from_screen(0, y)[1], - getCanvasHeight=lambda: self.canvas.props.y2, - setPoints=self.setPoints, - getDragRange=self.getDragRange, - ) - if not self.selected_points and self.selectManip: - self.selectManip.destroy() - self.selectManip = None - - self.selectionChanged() - - def getDragRange(self, idxs): - """ - if you're dragging these points, what's the most time you can move - left and right before colliding (exactly) with another - point - """ - maxLeft = maxRight = 99999 - cp = self.curve.points - for i in idxs: - nextStatic = i - while nextStatic >= 0 and nextStatic in idxs: - nextStatic -= 1 - if nextStatic >= 0: - maxLeft = min(maxLeft, cp[i][0] - cp[nextStatic][0]) - - nextStatic = i - while nextStatic <= len(cp) - 1 and nextStatic in idxs: - nextStatic += 1 - if nextStatic <= len(cp) - 1: - maxRight = min(maxRight, cp[nextStatic][0] - cp[i][0]) - return maxLeft, maxRight - - def setPoints(self, updates): - self.curve.set_points(updates) - - def selectionChanged(self): - if self.selectManip: - self.selectManip.update() - - def select_press(self, ev): - # todo: these select_ handlers are getting called on c-a-drag - # zooms too. the dispatching should be more selective than - # just calling both handlers all the time - self.print_state("select_press") - if self.dragging_dots: - return - if not self.selecting: - self.selecting = True - self.select_start = self.world_from_screen(ev.x, 0)[0] - #cursors.push(self,"gumby") - - def select_motion(self, ev): - if not self.selecting: - return - start = self.select_start - cur = self.world_from_screen(ev.x, 0)[0] - self.select_between(start, cur) - - def select_release(self, ev): - self.print_state("select_release") - - # dotrelease never gets called, but I can clear that state here - self.dragging_dots = False - - if not self.selecting: - return - #cursors.pop(self) - self.selecting = False - self.select_between(self.select_start, - self.world_from_screen(ev.x, 0)[0]) - - def sketch_press(self, ev): - self.sketch = Sketch(self, ev) - - def sketch_motion(self, ev): - if self.sketch: - self.sketch.motion(ev) - - def sketch_release(self, ev): - if self.sketch: - self.sketch.release(ev) - self.sketch = None - - def current_time(self): - return self._time - - def _coords(self): - z = self.zoomControl - ht = self.canvas.props.y2 - marginBottom = 3 if ht > 40 else 0 - marginTop = marginBottom - return z, ht, marginBottom, marginTop - - def screen_from_world(self, p): - z, ht, marginBottom, marginTop = self._coords() - return ((p[0] - z.start) / (z.end - z.start) * self.canvas.props.x2, - (ht - marginBottom) - p[1] * (ht - (marginBottom + marginTop))) - - def world_from_screen(self, x, y): - z, ht, marginBottom, marginTop = self._coords() - return (x / self.canvas.props.x2 * (z.end - z.start) + z.start, - ((ht - marginBottom) - y) / (ht - (marginBottom + marginTop))) - - def input_time(self, val, forceUpdate=False): - if self._time == val: - return - self.update_time_bar(val) - - def alive(self): - # Some handlers still run after a view is destroyed, which - # leads to crashes in somewhere like - # goocanvas_add_item. Workaround is to disable certain methods - # when the widget is gone. Correct solution would be to stop - # and free those handlers when the widget is gone. - return self.canvas.is_visible() - - def update_time_bar(self, t): - if not self.alive(): - return - if not getattr(self, 'timelineLine', None): - self.timelineGroup = GooCanvas.CanvasGroup( - parent=self.canvas.get_root_item()) - self.timelineLine = polyline_new_line(parent=self.timelineGroup, - points=Points([(0, 0), - (0, 0)]), - line_width=2, - stroke_color='red') - - try: - pts = [ - self.screen_from_world((t, 0)), - self.screen_from_world((t, 1)) - ] - except ZeroDivisionError: - pts = [(-1, -1), (-1, -1)] - self.timelineLine.set_property('points', Points(pts)) - - self._time = t - if self.knobEnabled: - self.delete('knob') - prevKey = self.curve.point_before(t) - if prevKey is not None: - pos = self.screen_from_world(prevKey) - self.create_oval(pos[0] - 8, - pos[1] - 8, - pos[0] + 8, - pos[1] + 8, - outline='#800000', - tags=('knob',)) - dispatcher.send("knob out", value=prevKey[1], curve=self.curve) - - def update_curve(self, *args): - if not getattr(self, '_pending_update', False): - self._pending_update = True - reactor.callLater(.01, self._update_curve) - - def _update_curve(self): - try: - self._update_curve2() - except Exception: - log.error("in update_curve on %s", self.curve.uri) - raise - - def _update_curve2(self): - if not getattr(self, '_pending_update', False): - return - self._pending_update = False - if not self.alive(): - return - if not self.redrawsEnabled: - print("no redrawsEnabled, skipping", self) - return - - visible_x = (self.world_from_screen(0, 0)[0], - self.world_from_screen(self.canvas.props.x2, 0)[0]) - - visible_idxs = self.curve.indices_between(visible_x[0], - visible_x[1], - beyond=1) - visible_points = [self.curve.points[i] for i in visible_idxs] - - if getattr(self, 'curveGroup', None): - self.curveGroup.remove() - self.curveGroup = GooCanvas.CanvasGroup( - parent=self.canvas.get_root_item()) - self.curveGroup.lower(None) - - self.canvas.set_property("background-color", - "gray20" if self.curve.muted else "black") - - self.update_time_bar(self._time) - self._draw_line(visible_points, area=True) - self._draw_markers( - self.markers.points[i] - for i in self.markers.indices_between(visible_x[0], visible_x[1])) - if self.canvas.props.y2 > 80: - self._draw_time_tics(visible_x) - - self.dots = {} # idx : canvas rectangle - - if len(visible_points) < 50 and not self.curve.muted: - self._draw_handle_points(visible_idxs, visible_points) - - self.selectionChanged() - - def is_music(self): - """are we one of the music curves (which might be drawn a bit - differently)""" - return self._isMusic - - def _draw_markers(self, pts): - colorMap = { - 'q': '#598522', - 'w': '#662285', - 'e': '#852922', - 'r': '#85225C', - 't': '#856B22', - 'y': '#227085', - } - for t, name in pts: - x = int(self.screen_from_world((t, 0))[0]) + .5 - polyline_new_line(self.curveGroup, - x, - 0, - x, - self.canvas.props.y2, - line_width=.4 if name in 'rty' else .8, - stroke_color=colorMap.get(name, 'gray')) - - def _draw_time_tics(self, visible_x): - tic = self._draw_one_tic - - tic(0, "0") - t1, t2 = visible_x - if t2 - t1 < 30: - for t in range(int(t1), int(t2) + 1): - tic(t, str(t)) - tic(introPad, str(introPad)) - - endtimes = dispatcher.send("get max time") - if endtimes: - endtime = endtimes[0][1] - tic(endtime, "end %.1f" % endtime) - tic(endtime - postPad, "post %.1f" % (endtime - postPad)) - - def _draw_one_tic(self, t, label): - try: - x = self.screen_from_world((t, 0))[0] - if not 0 <= x < self.canvas.props.x2: - return - x = max(5, x) # cheat left-edge stuff onscreen - except ZeroDivisionError: - x = -100 - - ht = self.canvas.props.y2 - polyline_new_line(self.curveGroup, - x, - ht, - x, - ht - 20, - line_width=.5, - stroke_color='gray70') - GooCanvas.CanvasText(parent=self.curveGroup, - fill_color="white", - anchor=GooCanvas.CanvasAnchorType.SOUTH, - font="ubuntu 7", - x=x + 3, - y=ht - 20, - text=label) - - def _draw_line(self, visible_points, area=False): - if not visible_points: - return - linepts = [] - step = 1 - linewidth = 1.5 - maxPointsToDraw = self.canvas.props.x2 / 2 - if len(visible_points) > maxPointsToDraw: - step = int(len(visible_points) / maxPointsToDraw) - linewidth = .8 - for p in visible_points[::step]: - try: - x, y = self.screen_from_world(p) - except ZeroDivisionError: - x = y = -100 - linepts.append((int(x) + .5, int(y) + .5)) - - if self.curve.muted: - fill = 'grey34' - else: - fill = 'white' - - if area: - try: - base = self.screen_from_world((0, 0))[1] - except ZeroDivisionError: - base = -100 - base = base + linewidth / 2 - areapts = linepts[:] - if len(areapts) >= 1: - areapts.insert(0, (0, areapts[0][1])) - areapts.append((self.canvas.props.x2, areapts[-1][1])) - polyline_new_line( - parent=self.curveGroup, - points=Points([(areapts[0][0], base)] + areapts + - [(areapts[-1][0], base)]), - close_path=True, - line_width=0, - # transparent as a workaround for - # covering some selectmanips (which - # become unclickable) - fill_color_rgba=0x00800080, - ) - - self.pl = polyline_new_line( - parent=self.curveGroup, - points=Points(linepts), - line_width=linewidth, - stroke_color=fill, - ) - - def _draw_handle_points(self, visible_idxs, visible_points): - for i, p in zip(visible_idxs, visible_points): - rad = 6 - worldp = p - try: - p = self.screen_from_world(p) - except ZeroDivisionError: - p = (-100, -100) - dot = GooCanvas.CanvasRect( - parent=self.curveGroup, - x=int(p[0] - rad) + .5, - y=int(p[1] - rad) + .5, - width=rad * 2, - height=rad * 2, - stroke_color='gray90', - fill_color='blue', - line_width=1, - #tags=('curve','point', 'handle%d' % i) - ) - - if worldp[1] == 0: - rad += 3 - GooCanvas.CanvasEllipse( - parent=self.curveGroup, - center_x=p[0], - center_y=p[1], - radius_x=rad, - radius_y=rad, - line_width=2, - stroke_color='#00a000', - #tags=('curve','point', 'handle%d' % i) - ) - dot.connect("button-press-event", self.dotpress, i) - #self.tag_bind('handle%d' % i,"", - # lambda ev,i=i: self.dotpress(ev,i)) - #self.tag_bind('handle%d' % i, "", - # lambda ev, i=i: self.remove_point_idx(i)) - - self.dots[i] = dot - - self.highlight_selected_dots() - - def find_index_near(self, x, y): - tags = self.gettags(self.find_closest(x, y)) - try: - handletags = [t for t in tags if t.startswith('handle')] - return int(handletags[0][6:]) - except IndexError: - raise ValueError("no point found") - - def new_point_at_mouse(self, ev): - p = self.world_from_screen(ev.x, ev.y) - x = p[0] - y = self.curve.eval(x) - self.add_point((x, y)) - - def add_points(self, pts): - idxs = [self.curve.insert_pt(p) for p in pts] - self.select_indices(idxs) - - def add_point(self, p): - self.add_points([p]) - - def add_marker(self, p): - self.markers.insert_pt(p) - - def remove_point_idx(self, *idxs): - idxs = list(idxs) - while idxs: - i = idxs.pop() - - self.curve.pop_point(i) - newsel = [] - newidxs = [] - for si in range(len(self.selected_points)): - sp = self.selected_points[si] - if sp == i: - continue - if sp > i: - sp -= 1 - newsel.append(sp) - for ii in range(len(idxs)): - if ii > i: - ii -= 1 - newidxs.append(idxs[ii]) - - self.select_indices(newsel) - idxs[:] = newidxs - - def highlight_selected_dots(self): - if not self.redrawsEnabled: - return - - for i, d in list(self.dots.items()): - if i in self.selected_points: - d.set_property('fill_color', 'red') - else: - d.set_property('fill_color', 'blue') - - def dotpress(self, r1, r2, ev, dotidx): - self.print_state("dotpress") - if dotidx not in self.selected_points: - self.select_indices([dotidx]) - - self.last_mouse_world = self.world_from_screen(ev.x, ev.y) - self.dragging_dots = True - - def select_between(self, start, end): - if start > end: - start, end = end, start - self.select_indices(self.curve.indices_between(start, end)) - - def onEnter(self, widget, event): - self.entered = True - - def onLeave(self, widget, event): - self.entered = False - - def onMotion(self, widget, event): - self.lastMouseX = event.x - - if event.state & Gdk.ModifierType.SHIFT_MASK and 1: # and B1 - self.sketch_motion(event) - return - - self.select_motion(event) - - if not self.dragging_dots: - return - if not event.state & 256: - return # not lmb-down - - # this way is accumulating error and also making it harder to - # undo (e.g. if the user moves far out of the window or - # presses esc or something). Instead, we should be resetting - # the points to their start pos plus our total offset. - cur = self.world_from_screen(event.x, event.y) - if self.last_mouse_world: - delta = (cur[0] - self.last_mouse_world[0], - cur[1] - self.last_mouse_world[1]) - else: - delta = 0, 0 - self.last_mouse_world = cur - - self.translate_points(delta) - - def translate_points(self, delta): - moved = False - - cp = self.curve.points - updates = [] - for idx in self.selected_points: - - newp = [cp[idx][0] + delta[0], cp[idx][1] + delta[1]] - - newp[1] = max(0, min(1, newp[1])) - - if idx > 0 and newp[0] <= cp[idx - 1][0]: - continue - if idx < len(cp) - 1 and newp[0] >= cp[idx + 1][0]: - continue - moved = True - updates.append((idx, tuple(newp))) - self.curve.set_points(updates) - return moved - - def unselect(self): - self.select_indices([]) - - def onScroll(self, widget, event): - t = self.world_from_screen(event.x, 0)[0] - self.zoomControl.zoom_about_mouse( - t, - factor=1.5 if event.direction == Gdk.ScrollDirection.DOWN else 1 / - 1.5) - # Don't actually scroll the canvas! (it shouldn't have room to - # scroll anyway, but it does because of some coordinate errors - # and borders and stuff) - return True - - def onRelease(self, widget, event): - self.print_state("dotrelease") - - if event.state & Gdk.ModifierType.SHIFT_MASK: # relese-B1 - self.sketch_release(event) - return - - self.select_release(event) - - if not self.dragging_dots: - return - self.last_mouse_world = None - self.dragging_dots = False - - -class CurveRow(object): - """ - one of the repeating curve rows (including widgets on the left) - - i wish these were in a list-style TreeView so i could set_reorderable on it - - please pack self.box - """ - - def __init__(self, graph, name, curve, markers, zoomControl): - self.graph = graph - self.name = name - self.box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) - self.box.set_border_width(1) - - self.cols = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) - self.box.add(self.cols) - - controls = Gtk.Frame() - controls.set_size_request(160, -1) - controls.set_shadow_type(Gtk.ShadowType.OUT) - self.cols.pack_start(controls, expand=False, fill=True, padding=0) - self.setupControls(controls, name, curve) - - self.curveView = Curveview(curve, - markers, - isMusic=name in ['music', 'smooth_music'], - zoomControl=zoomControl) - - self.initCurveView() - dispatcher.connect(self.rebuild, "all curves rebuild") - - def isFocus(self): - return self.curveView.widget.is_focus() - - def rebuild(self): - raise NotImplementedError('obsolete, if curves are drawing right') - self.curveView.rebuild() - self.initCurveView() - self.update_ui_to_collapsed_state() - - def destroy(self): - self.curveView.entered = False # help suppress bad position events - del self.curveView - self.box.destroy() - - def initCurveView(self): - self.curveView.widget.show() - self.setHeight(100) - self.cols.pack_start(self.curveView.widget, - expand=True, - fill=True, - padding=0) - - def setHeight(self, h): - self.curveView.widget.set_size_request(-1, h) - - # this should have been automatic when the size changed, but - # the signals for that are wrong somehow. - reactor.callLater(0, self.curveView.setCanvasToWidgetSize) - - def setupControls(self, controls, name, curve): - box = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) - controls.add(box) - - curve_name_label = Gtk.LinkButton() - print("need to truncate this name length somehow") - - def update_label(): - # todo: abort if we don't still exist... - p = curve_name_label.props - p.uri = curve.uri - p.label = self.graph.label(curve.uri) - - self.graph.addHandler(update_label) - - self.muted = Gtk.CheckButton("M") - self.muted.connect("toggled", self.sync_mute_to_curve) - dispatcher.connect(self.mute_changed, 'mute changed', sender=curve) - - box.pack_start(curve_name_label, expand=True, fill=True, padding=0) - box.pack_start(self.muted, expand=True, fill=True, padding=0) - - def onDelete(self): - self.curveView.onDelete() - - def sync_mute_to_curve(self, *args): - """send value from CheckButton to the master attribute inside Curve""" - new_mute = self.muted.get_active() - self.curveView.curve.muted = new_mute - - def update_mute_look(self): - """set colors on the widgets in the row according to self.muted.get()""" - # not yet ported for gtk - return - if self.curveView.curve.muted: - new_bg = 'grey20' - else: - new_bg = 'normal' - - for widget in self.widgets: - widget['bg'] = new_bg - - def mute_changed(self): - """call this if curve.muted changed""" - self.muted.set_active(self.curveView.curve.muted) - #self.update_mute_look() - - -class Curvesetview(object): - """ - - """ - - def __init__(self, graph, curvesVBox, zoomControlBox, curveset): - self.graph = graph - self.live = True - self.curvesVBox = curvesVBox - self.curveset = curveset - self.allCurveRows = set() - self.visibleHeight = 1000 - - self.zoomControl = self.initZoomControl(zoomControlBox) - self.zoomControl.redrawzoom() - - for uri, label, curve in curveset.currentCurves(): - self.add_curve(uri, label, curve) - - dispatcher.connect(self.clear_curves, "clear_curves") - dispatcher.connect(self.add_curve, "add_curve", sender=self.curveset) - dispatcher.connect(self.set_featured_curves, "set_featured_curves") - dispatcher.connect(self.song_has_changed, "song_has_changed") - - self.newcurvename = Gtk.EntryBuffer.new("", 0) - - eventBox = self.curvesVBox.get_parent() - eventBox.connect("key-press-event", self.onKeyPress) - eventBox.connect("button-press-event", self.takeFocus) - - self.watchCurveAreaHeight() - - def __del__(self): - print("del curvesetview", id(self)) - - def initZoomControl(self, zoomControlBox): - import light9.curvecalc.zoomcontrol - imp.reload(light9.curvecalc.zoomcontrol) - zoomControl = light9.curvecalc.zoomcontrol.ZoomControl() - zoomControlBox.add(zoomControl.widget) - zoomControl.widget.show_all() - return zoomControl - - def clear_curves(self): - """curveset is about to re-add all new curves""" - while self.allCurveRows: - self.allCurveRows.pop().destroy() - - def song_has_changed(self): - self.zoomControl.redrawzoom() - - def takeFocus(self, *args): - """the whole curveset's eventbox is what gets the focus, currently, so - keys like 'c' can work in it""" - dispatcher.send("all curves lose selection") - self.curvesVBox.get_parent().grab_focus() - - def curveRow_from_name(self, name): - for cr in self.allCurveRows: - if cr.name == name: - return cr - raise ValueError("couldn't find curveRow named %r" % name) - - def set_featured_curves(self, curveNames): - """bring these curves to the top of the stack""" - for n in curveNames[::-1]: - self.curvesVBox.reorder_child( - self.curveRow_from_name(n).box, Gtk.PACK_START) - - def onKeyPress(self, widget, event): - if not self.live: # workaround for old instances living past reload() - return - - #r = self.row_under_mouse() - #key = event.string - pass # no handlers right now - - def row_under_mouse(self): - x, y = self.curvesVBox.get_pointer() - for r in self.allCurveRows: - inRowX, inRowY = self.curvesVBox.translate_coordinates(r.box, x, y) - alloc = r.box.get_allocation() - if 0 <= inRowX < alloc.width and 0 <= inRowY < alloc.height: - return r - raise ValueError("no curveRow is under the mouse") - - def focus_entry(self): - self.entry.focus() - - def new_curve(self, event): - self.curveset.new_curve(self.newcurvename.get()) - self.newcurvename.set('') - - def add_curve(self, uri, label, curve): - if isinstance(label, Literal): - label = str(label) - - f = CurveRow(self.graph, label, curve, self.curveset.markers, - self.zoomControl) - self.curvesVBox.pack_start(f.box, expand=True, fill=True, padding=0) - f.box.show_all() - self.allCurveRows.add(f) - self.setRowHeights() - f.curveView.goLive() - - def watchCurveAreaHeight(self): - - def sizeEvent(w, size): - # this is firing really often - if self.visibleHeight == size.height: - return - log.debug("size.height is new: %s", size.height) - self.visibleHeight = size.height - self.setRowHeights() - - visibleArea = self.curvesVBox.get_parent().get_parent() - visibleArea.connect('size-allocate', sizeEvent) - - dispatcher.connect(self.setRowHeights, "curve row focus change") - - def setRowHeights(self): - nRows = len(self.allCurveRows) - if not nRows: - return - anyFocus = any(r.isFocus() for r in self.allCurveRows) - - evenHeight = max(14, self.visibleHeight // nRows) - 3 - if anyFocus: - focusHeight = max(100, evenHeight) - if nRows > 1: - otherHeight = max(14, (self.visibleHeight - focusHeight) // - (nRows - 1)) - 3 - else: - otherHeight = evenHeight - for row in self.allCurveRows: - row.setHeight(focusHeight if row.isFocus() else otherHeight) - - def row(self, name): - if isinstance(name, Literal): - name = str(name) - matches = [r for r in self.allCurveRows if r.name == name] - if not matches: - raise ValueError("no curveRow named %r. only %s" % - (name, [r.name for r in self.allCurveRows])) - return matches[0] - - def goLive(self): - """for startup performance, none of the curves redraw - themselves until this is called once (and then they're normal)""" - - for cr in self.allCurveRows: - cr.curveView.goLive() - - def onDelete(self): - for r in self.allCurveRows: - r.onDelete() diff -r 623836db99af -r 4556eebe5d73 light9/curvecalc/musicaccess.py --- a/light9/curvecalc/musicaccess.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,83 +0,0 @@ -import json -from louie import dispatcher -from rdflib import URIRef -from light9 import networking -from twisted.internet import reactor -from twisted.web.client import Agent -from twisted.internet.protocol import Protocol -from twisted.internet.defer import Deferred -from zope.interface import implements -from twisted.internet.defer import succeed -from twisted.web.iweb import IBodyProducer - - -class GatherJson(Protocol): - """calls back the 'finished' deferred with the parsed json data we - received""" - - def __init__(self, finished): - self.finished = finished - self.buf = "" - - def dataReceived(self, bytes): - self.buf += bytes - - def connectionLost(self, reason): - self.finished.callback(json.loads(self.buf)) - - -class StringProducer(object): - # http://twistedmatrix.com/documents/current/web/howto/client.html - implements(IBodyProducer) - - def __init__(self, body): - self.body = body - self.length = len(body) - - def startProducing(self, consumer): - consumer.write(self.body) - return succeed(None) - - def pauseProducing(self): - pass - - def stopProducing(self): - pass - - -class Music: - - def __init__(self): - self.recenttime = 0 - self.player = Agent(reactor) - self.timePath = networking.musicPlayer.path("time") - - def current_time(self): - """return deferred which gets called with the current - time. This gets called really often""" - d = self.player.request("GET", self.timePath) - d.addCallback(self._timeReturned) - return d - - def _timeReturned(self, response): - done = Deferred() - done.addCallback(self._bodyReceived) - response.deliverBody(GatherJson(done)) - return done - - def _bodyReceived(self, data): - if 't' in data: - dispatcher.send("input time", val=data['t']) - if 'song' in data and data['song']: - dispatcher.send("current_player_song", song=URIRef(data['song'])) - return data['t'] # pass along to the real receiver - - def playOrPause(self, t=None): - if t is None: - # could be better - self.current_time().addCallback(lambda t: self.playOrPause(t)) - else: - self.player.request("POST", - networking.musicPlayer.path("seekPlayOrPause"), - bodyProducer=StringProducer(json.dumps({"t": - t}))) diff -r 623836db99af -r 4556eebe5d73 light9/curvecalc/output.py --- a/light9/curvecalc/output.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,72 +0,0 @@ -import time, logging -from twisted.internet import reactor -from light9 import Submaster, dmxclient - -from louie import dispatcher -log = logging.getLogger("output") - - -class Output(object): - lastsendtime = 0 - lastsendlevs = None - - def __init__(self, graph, session, music, curveset, currentSubterms): - self.graph, self.session, self.music = graph, session, music - self.currentSubterms = currentSubterms - self.curveset = curveset - - self.recent_t = [] - self.later = None - - self.update() - - def update(self): - d = self.music.current_time() - d.addCallback(self.update2) - d.addErrback(self.updateerr) - - def updateerr(self, e): - - print(e.getTraceback()) - dispatcher.send("update status", val=e.getErrorMessage()) - if self.later and not self.later.cancelled and not self.later.called: - self.later.cancel() - self.later = reactor.callLater(1, self.update) - - def update2(self, t): - # spot alsa soundcard offset is always 0, we get times about a - # second ahead of what's really getting played - #t = t - .7 - dispatcher.send("update status", - val="ok: receiving time from music player") - if self.later and not self.later.cancelled and not self.later.called: - self.later.cancel() - - self.later = reactor.callLater(.02, self.update) - - self.recent_t = self.recent_t[-50:] + [t] - period = (self.recent_t[-1] - self.recent_t[0]) / len(self.recent_t) - dispatcher.send("update period", val=period) - self.send_dmx(t) - - def send_dmx(self, t): - dispatcher.send("curves to sliders", t=t) - - if not self.currentSubterms: - return - - scaledsubs = [] - for st in self.currentSubterms: - scl = st.scaled(t) - scaledsubs.append(scl) - - out = Submaster.sub_maxes(*scaledsubs) - levs = out.get_levels() - now = time.time() - if now - self.lastsendtime > 5 or levs != self.lastsendlevs: - dispatcher.send("output levels", val=levs) - dmxclient.outputlevels(out.get_dmx_list(), - twisted=1, - clientid='curvecalc') - self.lastsendtime = now - self.lastsendlevs = levs diff -r 623836db99af -r 4556eebe5d73 light9/curvecalc/subterm.py --- a/light9/curvecalc/subterm.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,144 +0,0 @@ -import logging -from rdflib import Literal -from louie import dispatcher -import light9.Effects -from light9 import Submaster -from light9.Patch import get_dmx_channel -from rdfdb.patch import Patch -from light9.namespaces import L9 -log = logging.getLogger() - - -class Expr(object): - """singleton, provides functions for use in subterm expressions, - e.g. chases""" - - def __init__(self): - self.effectGlobals = light9.Effects.configExprGlobals() - - def exprGlobals(self, startDict, t): - """globals dict for use by expressions""" - - glo = startDict.copy() - - # add in functions from Effects - glo.update(self.effectGlobals) - - 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 aft(t, x, smooth=0): - left = x - smooth / 2 - right = x + smooth / 2 - if left < t < right: - return light9.Effects.smoove((t - left) / (right - left)) - return t > x - - glo['aft'] = lambda x, smooth=0: aft(t, x, smooth) - - 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 - - -exprglo = Expr() - - -class Subterm(object): - """one Submaster and its expression evaluator""" - - def __init__(self, graph, subterm, saveContext, curveset): - self.graph, self.uri = graph, subterm - self.saveContext = saveContext - self.curveset = curveset - self.ensureExpression(saveContext) - - self.submasters = Submaster.get_global_submasters(self.graph) - - def ensureExpression(self, saveCtx): - with self.graph.currentState(tripleFilter=(self.uri, None, - None)) as current: - if current.value(self.uri, L9['expression']) is None: - self.graph.patch( - Patch(addQuads=[ - (self.uri, L9['expression'], Literal("..."), saveCtx), - ])) - - def scaled(self, t): - with self.graph.currentState(tripleFilter=(self.uri, None, - None)) as current: - subexpr_eval = self.eval(current, t) - # we prevent any exceptions from escaping, since they cause us to - # stop sending levels - try: - if isinstance(subexpr_eval, Submaster.Submaster): - # if the expression returns a submaster, just return it - return subexpr_eval - else: - # otherwise, return our submaster multiplied by the value - # returned - if subexpr_eval == 0: - return Submaster.Submaster("zero", {}) - subUri = current.value(self.uri, L9['sub']) - sub = self.submasters.get_sub_by_uri(subUri) - return sub * subexpr_eval - except Exception as e: - dispatcher.send("expr_error", sender=self.uri, exc=repr(e)) - return Submaster.Submaster(name='Error: %s' % str(e), levels={}) - - def curves_used_by_expr(self): - """names of curves that are (maybe) used in this expression""" - - with self.graph.currentState(tripleFilter=(self.uri, None, - None)) as current: - expr = current.value(self.uri, L9['expression']) - - used = [] - for name in self.curveset.curveNamesInOrder(): - if name in expr: - used.append(name) - return used - - def eval(self, current, t): - """current graph is being passed as an optimization. It should be - equivalent to use self.graph in here.""" - - objs = list(current.objects(self.uri, L9['expression'])) - if len(objs) > 1: - raise ValueError("found multiple expressions for %s: %s" % - (self.uri, objs)) - - expr = current.value(self.uri, L9['expression']) - if not expr: - dispatcher.send("expr_error", - sender=self.uri, - exc="no expr, using 0") - return 0 - glo = self.curveset.globalsdict() - glo['t'] = t - - glo = exprglo.exprGlobals(glo, t) - glo['getsub'] = lambda name: self.submasters.get_sub_by_name(name) - glo['chan'] = lambda name: Submaster.Submaster( - "chan", {get_dmx_channel(name): 1}) - - try: - self.lasteval = eval(expr, glo) - except Exception as e: - dispatcher.send("expr_error", sender=self.uri, exc=e) - return Submaster.Submaster("zero", {}) - else: - dispatcher.send("expr_error", sender=self.uri, exc="ok") - return self.lasteval - - def __repr__(self): - return "" % self.uri diff -r 623836db99af -r 4556eebe5d73 light9/curvecalc/subtermview.py --- a/light9/curvecalc/subtermview.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,134 +0,0 @@ -import logging -from gi.repository import Gtk -from louie import dispatcher -from rdflib import Literal, URIRef -from light9.namespaces import L9 -log = logging.getLogger() - -# inspired by http://www.daa.com.au/pipermail/pygtk/2008-August/015772.html -# keeping a ref to the __dict__ of the object stops it from getting zeroed -keep = [] - - -class Subexprview(object): - - def __init__(self, graph, ownerSubterm, saveContext, curveset): - self.graph, self.ownerSubterm = graph, ownerSubterm - self.saveContext = saveContext - self.curveset = curveset - - self.box = Gtk.HBox() - - self.entryBuffer = Gtk.EntryBuffer("", -1) - self.entry = Gtk.Entry() - self.error = Gtk.Label("") - - self.box.pack_start(self.entry, expand=True) - self.box.pack_start(self.error, expand=False) - - self.entry.set_buffer(self.entryBuffer) - self.graph.addHandler(self.set_expression_from_graph) - self.entryBuffer.connect("deleted-text", self.entry_changed) - self.entryBuffer.connect("inserted-text", self.entry_changed) - - self.entry.connect("focus-in-event", self.onFocus) - - dispatcher.connect(self.exprError, - "expr_error", - sender=self.ownerSubterm) - keep.append(self.__dict__) - - def onFocus(self, *args): - curveNames = self.curveset.curveNamesInOrder() - currentExpr = self.entryBuffer.get_text() - - usedCurves = sorted([n for n in curveNames if n in currentExpr]) - - dispatcher.send("set_featured_curves", curveNames=usedCurves) - - def exprError(self, exc): - self.error.set_text(str(exc)) - - def set_expression_from_graph(self): - e = str(self.graph.value(self.ownerSubterm, L9['expression'])) - print("from graph, set to %r" % e) - - if e != self.entryBuffer.get_text(): - self.entryBuffer.set_text(e, len(e)) - - def entry_changed(self, *args): - log.info("want to patch to %r", self.entryBuffer.get_text()) - self.graph.patchObject(self.saveContext, self.ownerSubterm, - L9['expression'], - Literal(self.entryBuffer.get_text())) - - -class Subtermview(object): - """ - has .label and .exprView widgets for you to put in a table - """ - - def __init__(self, st, curveset): - self.subterm = st - self.graph = st.graph - - self.label = Gtk.Label("sub") - self.graph.addHandler(self.setName) - - self.label.drag_dest_set(flags=Gtk.DEST_DEFAULT_ALL, - targets=[('text/uri-list', 0, 0)], - actions=Gtk.gdk.ACTION_COPY) - self.label.connect("drag-data-received", self.onDataReceivedOnLabel) - - sev = Subexprview(self.graph, self.subterm.uri, - self.subterm.saveContext, curveset) - self.exprView = sev.box - - def onDataReceivedOnLabel(self, widget, context, x, y, selection, - targetType, time): - self.graph.patchObject(self.subterm.saveContext, self.subterm.uri, - L9['sub'], URIRef(selection.data.strip())) - - def setName(self): - # some of this could be pushed into Submaster - sub = self.graph.value(self.subterm.uri, L9['sub']) - if sub is None: - tail = self.subterm.uri.rsplit('/', 1)[-1] - self.label.set_text("no sub (%s)" % tail) - return - label = self.graph.label(sub) - if label is None: - self.label.set_text("sub %s has no label" % sub) - return - self.label.set_text(label) - - -def add_one_subterm(subterm, curveset, master, show=False): - stv = Subtermview(subterm, curveset) - - y = master.get_property('n-rows') - master.attach(stv.label, 0, 1, y, y + 1, xoptions=0, yoptions=0) - master.attach(stv.exprView, 1, 2, y, y + 1, yoptions=0) - scrollToRowUponAdd(stv.label) - if show: - master.show_all() - - -def scrollToRowUponAdd(widgetInRow): - """when this table widget is ready, scroll the table so we can see it""" - - # this doesn't work right, yet - return - - vp = widgetInRow - while vp.get_name() != 'GtkViewport': - log.info("walk %s", vp.get_name()) - vp = vp.get_parent() - adj = vp.props.vadjustment - - def firstExpose(widget, event, adj, widgetInRow): - log.info("scroll %s", adj.props.value) - adj.props.value = adj.props.upper - widgetInRow.disconnect(handler) - - handler = widgetInRow.connect('expose-event', firstExpose, adj, widgetInRow) diff -r 623836db99af -r 4556eebe5d73 light9/curvecalc/zoomcontrol.py --- a/light9/curvecalc/zoomcontrol.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,380 +0,0 @@ -from gi.repository import GooCanvas -import louie as dispatcher -from light9.curvecalc import cursors -from lib.goocanvas_compat import Points, polyline_new_line -from twisted.internet import reactor - - -class ZoomControl(object): - """ - please pack .widget - """ - - mintime = 0 - - def maxtime(): - doc = "seconds at the right edge of the bar" - - def fget(self): - return self._maxtime - - def fset(self, value): - self._maxtime = value - self.redrawzoom() - - return locals() - - maxtime = property(**maxtime()) - - _end = _start = 0 - - def start(): - - def fget(self): - return self._start - - def fset(self, v): - v = max(self.mintime, v) - # don't protect for start (self.end - vis_seconds * .6): - self.offset = t - margin - - self.redrawzoom() - - def see_time_until_end(self, t=None): - """defaults to current time""" - if t is None: - t = self.lastTime - self.start = t - 2 - self.end = self.maxtime - - self.redrawzoom() - - def input_time(self, val): - """move time cursor to this time""" - self.lastTime = val - try: - x = self.can_for_t(self.lastTime) - except ZeroDivisionError: - x = -100 - self.time.set_property("points", Points([(x, 0), - (x, self.size.height)])) - - def press(self, ev, attr): - self.adjustingattr = attr - - def release(self, widget, ev): - if hasattr(self, 'adjustingattr'): - del self.adjustingattr - if hasattr(self, 'lastx'): - del self.lastx - - def adjust(self, widget, ev): - - if not hasattr(self, 'adjustingattr'): - return - attr = self.adjustingattr - - if not hasattr(self, 'lastx'): - self.lastx = ev.x - new = self.can_for_t(getattr(self, attr)) + (ev.x - self.lastx) - self.lastx = ev.x - setattr(self, attr, self.t_for_can(new)) - self.redrawzoom() - - def can_for_t(self, t): - a, b = self.mintime, self.maxtime - return (t - a) / (b - a) * (self.size.width - 30) + 20 - - def t_for_can(self, x): - a, b = self.mintime, self.maxtime - return (x - 20) / (self.size.width - 30) * (b - a) + a - - def redrawzoom(self, *args): - # often, this was clearing the zoom widget and not repainting right - reactor.callLater(0, self._redrawzoom) - - def _redrawzoom(self): - """redraw pieces based on start/end""" - self.size = self.widget.get_allocation() - dispatcher.send("zoom changed") - if not hasattr(self, 'created'): - return - y1, y2 = 3, self.size.height - 3 - lip = 6 - try: - scan = self.can_for_t(self.start) - ecan = self.can_for_t(self.end) - except ZeroDivisionError: - # todo: set the zoom to some clear null state - return - - self.leftbrack.set_property( - "points", - Points([(scan + lip, y1), (scan, y1), (scan, y2), - (scan + lip, y2)])) - self.rightbrack.set_property( - "points", - Points([(ecan - lip, y1), (ecan, y1), (ecan, y2), - (ecan - lip, y2)])) - self.shade.set_properties(x=scan + 5, - y=y1 + lip, - width=max(0, ecan - 5 - (scan + 5)), - height=max(0, y2 - lip - (y1 + lip))) - - self.redrawTics() - - def redrawTics(self): - if hasattr(self, 'ticsGroup'): - self.ticsGroup.remove() - self.ticsGroup = GooCanvas.CanvasGroup(parent=self.root) - - lastx = -1000 - - for t in range(0, int(self.maxtime)): - x = self.can_for_t(t) - if 0 < x < self.size.width and x - lastx > 30: - txt = str(t) - if lastx == -1000: - txt = txt + "sec" - GooCanvas.CanvasPolyline(parent=self.ticsGroup, - points=Points([(x, 0), (x, 15)]), - line_width=.8, - stroke_color='black') - GooCanvas.CanvasText(parent=self.ticsGroup, - x=x, - y=self.size.height - 1, - anchor=GooCanvas.CanvasAnchorType.SOUTH, - text=txt, - font='ubuntu 7') - lastx = x - - -class RegionZoom: - """rigs c-a-b1 to drag out an area to zoom to. also catches other types of drag events, like b1 drag for selecting points - - this is used with Curveview - """ - - def __init__(self, canvas, world_from_screen, screen_from_world): - self.canvas, self.world_from_screen = canvas, world_from_screen - self.screen_from_world = screen_from_world - - for evtype, method in [("ButtonPress-1", self.press), - ("Motion", self.motion), - ("ButtonRelease-1", self.release)]: - #canvas.bind("" % evtype, method, add=True) - if 1 or evtype != "ButtonPress-1": - canvas.bind("<%s>" % evtype, method, add=True) - - canvas.bind("", self.finish) - self.start_t = self.old_cursor = None - self.state = self.mods = None - - def press(self, ev): - if self.state is not None: - self.finish() - - if ev.state == 12: - self.mods = "c-a" - elif ev.state == 13: - # todo: right now this never happens because only the - # sketching handler gets the event - self.mods = "c-s-a" - elif ev.state == 0: - return # no - self.mods = "none" - else: - return - self.state = "buttonpress" - - self.start_t = self.end_t = self.world_from_screen(ev.x, 0)[0] - self.start_x = ev.x - can = self.canvas - - for pos in ('start_t', 'end_t', 'hi', 'lo'): - can.create_line(0, - 0, - 50, - 50, - width=3, - fill='yellow', - tags=("regionzoom", pos)) - # if updatelines isn't called here, subsequent updatelines - # will fail for reasons i don't understand - self.updatelines() - - # todo: just holding the modifiers ought to turn on the zoom - # cursor (which is not finished) - cursors.push(can, "@light9/cursor1.xbm") - - def updatelines(self): - - # better would be a gray25 rectangle over the region - - can = self.canvas - pos_x = {} - height = can.winfo_height() - for pos in ('start_t', 'end_t'): - pos_x[pos] = x = self.screen_from_world((getattr(self, pos), 0))[0] - cid = can.find_withtag("regionzoom && %s" % pos) - can.coords(cid, x, 0, x, height) - - for tag, frac in [('hi', .1), ('lo', .9)]: - cid = can.find_withtag("regionzoom && %s" % tag) - can.coords(cid, pos_x['start_t'], frac * height, pos_x['end_t'], - frac * height) - - def motion(self, ev): - if self.state != "buttonpress": - return - - self.end_t = self.world_from_screen(ev.x, 0)[0] - self.updatelines() - - def release(self, ev): - if self.state != "buttonpress": - return - - if abs(self.start_x - ev.x) < 10: - # clicked - if self.mods in ("c-a", "c-s-a"): - factor = 1 / 1.5 - if self.mods == "c-s-a": - factor = 1.5 # c-s-a-b1 zooms out - dispatcher.send("zoom about mouse", - t=self.start_t, - factor=factor) - - self.finish() - return - - start, end = min(self.start_t, self.end_t), max(self.start_t, - self.end_t) - if self.mods == "c-a": - dispatcher.send("zoom to range", start=start, end=end) - elif self.mods == "none": - dispatcher.send("select between", start=start, end=end) - self.finish() - - def finish(self, *ev): - if self.state is not None: - self.state = None - self.canvas.delete("regionzoom") - self.start_t = None - cursors.pop(self.canvas) diff -r 623836db99af -r 4556eebe5d73 light9/dmxchanedit.py --- a/light9/dmxchanedit.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,243 +0,0 @@ -""" - -widget to show all dmx channel levels and allow editing. levels might -not actually match what dmxserver is outputting. - -proposal for new focus and edit system: -- rows can be selected -- the chan number or label can be used to select rows. dragging over rows brings all of them into or out of the current selection -- numbers drag up and down (like today) -- if you drag a number in a selected row, all the selected numbers change -- if you start dragging a number in an unselected row, your row becomes the new selection and then the edit works - - -proposal for new attribute system: -- we always want to plan some attributes for each light: where to center; what stage to cover; what color gel to apply; whether the light is burned out -- we have to stop packing these into the names. Names should be like 'b33' or 'blue3' or just '44'. maybe 'blacklight'. - -""" - -import tkinter as tk -from rdflib import RDF -import math, logging -from decimal import Decimal -from light9.namespaces import L9 -log = logging.getLogger('dmxchanedit') -stdfont = ('Arial', 7) - - -def gradient(lev, low=(80, 80, 180), high=(255, 55, 50)): - out = [int(l + lev * (h - l)) for h, l in zip(high, low)] - col = "#%02X%02X%02X" % tuple(out) - return col - - -class Onelevel(tk.Frame): - """a name/level pair - - source data is like this: - ch:b11-c a :Channel; - :output dmx:c54; - rdfs:label "b11-c" . - - and the level is like this: - - ?editor :currentSub ?sub . - ?sub :lightLevel [:channel ?ch; :level ?level] . - - levels come in with self.setTo and go out by the onLevelChange - callback. This object does not use the graph for level values, - which I'm doing for what I think is efficiency. Unclear why I - didn't use Observable for that API. - """ - - def __init__(self, parent, graph, channelUri, onLevelChange): - tk.Frame.__init__(self, parent, height=20) - self.graph = graph - self.onLevelChange = onLevelChange - self.uri = channelUri - self.currentLevel = 0 # the level we're displaying, 0..1 - - # no statement yet - self.channelnum = int( - self.graph.value(self.uri, L9['output']).rsplit('/c')[-1]) - - # 3 widgets, left-to-right: - - # channel number -- will turn yellow when being altered - self.num_lab = tk.Label(self, - text=str(self.channelnum), - width=3, - bg='grey40', - fg='white', - font=stdfont, - padx=0, - pady=0, - bd=0, - height=1) - self.num_lab.pack(side='left') - - # text description of channel - self.desc_lab = tk.Label(self, - width=14, - font=stdfont, - anchor='w', - padx=0, - pady=0, - bd=0, - height=1, - bg='black', - fg='white') - self.graph.addHandler(self.updateLabel) - self.desc_lab.pack(side='left') - - # current level of channel, shows intensity with color - self.level_lab = tk.Label(self, - width=3, - bg='lightBlue', - anchor='e', - font=stdfont, - padx=1, - pady=0, - bd=0, - height=1) - self.level_lab.pack(side='left') - - self.setupmousebindings() - - def updateLabel(self): - self.desc_lab.config(text=self.graph.label(self.uri)) - - def setupmousebindings(self): - - def b1down(ev): - self.desc_lab.config(bg='cyan') - self._start_y = ev.y - self._start_lev = self.currentLevel - - def b1motion(ev): - delta = self._start_y - ev.y - self.setlevel(max(0, min(1, self._start_lev + delta * .005))) - - def b1up(ev): - self.desc_lab.config(bg='black') - - def b3up(ev): - self.setlevel(0.0) - - def b3down(ev): - self.setlevel(1.0) - - def b2down(ev): # same thing for now - self.setlevel(1.0) - - # make the buttons work in the child windows - for w in self.winfo_children(): - for e, func in (('', - b1down), ('', - b1motion), ('', b1up), - ('', - b2down), ('', - b3up), ('', b3down)): - - w.bind(e, func) - - def colorlabel(self): - """color the level label based on its own text (which is 0..100)""" - txt = self.level_lab['text'] or "0" - lev = float(txt) / 100 - self.level_lab.config(bg=gradient(lev)) - - def setlevel(self, newlev): - """UI received a level change, which we put in the graph""" - self.onLevelChange(self.uri, newlev) - - def setTo(self, newLevel): - """levelbox saw a change in the graph""" - self.currentLevel = min(1, max(0, newLevel)) - newLevel = "%d" % (self.currentLevel * 100) - olddisplay = self.level_lab.cget('text') - if newLevel != olddisplay: - self.level_lab.config(text=newLevel) - self.colorlabel() - - -class Levelbox(tk.Frame): - """ - this also watches all the levels in the sub and sets the boxes when they change - """ - - def __init__(self, parent, graph, currentSub): - """ - currentSub is an Observable(PersistentSubmaster) - """ - tk.Frame.__init__(self, parent) - - self.currentSub = currentSub - self.graph = graph - graph.addHandler(self.updateChannels) - - self.currentSub.subscribe(lambda _: graph.addHandler(self. - updateLevelValues)) - - def updateChannels(self): - """(re)make Onelevel boxes for the defined channels""" - - [ch.destroy() for ch in self.winfo_children()] - self.levelFromUri = {} # channel : OneLevel - - chans = list(self.graph.subjects(RDF.type, L9.Channel)) - chans.sort( - key=lambda c: int(self.graph.value(c, L9.output).rsplit('/c')[-1])) - cols = 2 - rows = int(math.ceil(len(chans) / cols)) - - def make_frame(parent): - f = tk.Frame(parent, bd=0, bg='black') - f.pack(side='left') - return f - - columnFrames = [make_frame(self) for x in range(cols)] - - for i, channel in enumerate(chans): # sort? - # frame for this channel - f = Onelevel(columnFrames[i // rows], self.graph, channel, - self.onLevelChange) - - self.levelFromUri[channel] = f - f.pack(side='top') - - def updateLevelValues(self): - """set UI level from graph""" - submaster = self.currentSub() - if submaster is None: - return - sub = submaster.uri - if sub is None: - raise ValueError("currentSub is %r" % submaster) - - remaining = set(self.levelFromUri.keys()) - for ll in self.graph.objects(sub, L9['lightLevel']): - chan = self.graph.value(ll, L9['channel']) - try: - lev = self.graph.value(ll, L9['level']).toPython() - except AttributeError as e: - log.error('on lightlevel %r:', ll) - log.exception(e) - continue - if isinstance(lev, Decimal): - lev = float(lev) - assert isinstance(lev, (int, float)), repr(lev) - try: - self.levelFromUri[chan].setTo(lev) - remaining.remove(chan) - except KeyError as e: - log.exception(e) - for channel in remaining: - self.levelFromUri[channel].setTo(0) - - def onLevelChange(self, chan, newLevel): - """UI received a change which we put in the graph""" - if self.currentSub() is None: - raise ValueError("no currentSub in Levelbox") - self.currentSub().editLevel(chan, newLevel) diff -r 623836db99af -r 4556eebe5d73 light9/dmxclient.py --- a/light9/dmxclient.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,76 +0,0 @@ -""" module for clients to use for easy talking to the dmx -server. sending levels is now a simple call to -dmxclient.outputlevels(..) - -client id is formed from sys.argv[0] and the PID. """ - -import xmlrpc.client, os, sys, socket, time, logging -from twisted.internet import defer -from txzmq import ZmqEndpoint, ZmqFactory, ZmqPushConnection -import json - -from light9 import networking -_dmx = None -log = logging.getLogger('dmxclient') - -procname = os.path.basename(sys.argv[0]) -procname = procname.replace('.py', '') -_id = "%s-%s-%s" % (procname, socket.gethostname(), os.getpid()) - - -class TwistedZmqClient(object): - - def __init__(self, service): - zf = ZmqFactory() - e = ZmqEndpoint('connect', 'tcp://%s:%s' % (service.host, service.port)) - - class Push(ZmqPushConnection): - pass # highWaterMark = 3000 - - self.conn = Push(zf, e) - - def send(self, clientid, levellist): - self.conn.push( - json.dumps({ - 'clientid': clientid, - 'levellist': levellist - })) - - -def outputlevels(levellist, twisted=0, clientid=_id): - """present a list of dmx channel levels, each scaled from - 0..1. list can be any length- it will apply to the first len() dmx - channels. - - if the server is not found, outputlevels will block for a - second.""" - - global _dmx, _id - - if _dmx is None: - url = networking.dmxServer.url - if not twisted: - _dmx = xmlrpc.client.Server(url) - else: - _dmx = TwistedZmqClient(networking.dmxServerZmq) - - if not twisted: - try: - _dmx.outputlevels(clientid, levellist) - except socket.error as e: - log.error("dmx server error %s, waiting" % e) - time.sleep(1) - except xmlrpc.client.Fault as e: - log.error("outputlevels had xml fault: %s" % e) - time.sleep(1) - else: - _dmx.send(clientid, levellist) - return defer.succeed(None) - - -dummy = os.getenv('DMXDUMMY') -if dummy: - print("dmxclient: DMX is in dummy mode.") - - def outputlevels(*args, **kw): # noqa - pass diff -r 623836db99af -r 4556eebe5d73 light9/editchoice.py --- a/light9/editchoice.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,118 +0,0 @@ -import tkinter as tk -from rdflib import URIRef -from light9.tkdnd import dragSourceRegister, dropTargetRegister - - -class Local(object): - """placeholder for the local uri that EditChoice does not - manage. Set resourceObservable to Local to indicate that you're - unlinked""" - - -class EditChoice(object): - """ - Observable <-> linker UI - - widget for tying some UI to a shared resource for editing, or - unlinking it (which means associating it with a local resource - that's not named or shared). This object does not own the choice - of resource; the caller does. - - UI actions: - - drag a uri on here to make it the one we're editing - - - button to clear the currentSub (putting it back to - sessionLocalSub, and also resetting sessionLocalSub to be empty - again) - - - drag the sub uri off of here to send it to another receiver, - but, if we're in local mode, the local sub should not be so - easily addressable. Maybe you just can't drag it off. - - - Todo: - - - filter by type so you can't drag a curve onto a subcomposer - - - 'save new' : make a new sub: transfers the current data (from a shared sub or - from the local one) to the new sub. If you're on a local sub, - the new sub is named automatically, ideally something brief, - pretty distinct, readable, and based on the lights that are - on. If you're on a named sub, the new one starts with a - 'namedsub 2' style name. The uri can also be with a '2' suffix, - although maybe that will be stupid. If you change the name - before anyone knows about this uri, we could update the current - sub's uri to a slug of the new label. - - - rename this sub: not available if you're on a local sub. Sets - the label of a named sub. Might update the uri of the named sub - if it's new enough that no one else would have that uri. Not - sure where we measure that 'new enough' value. Maybe track if - the sub has 'never been dragged out of this subcomposer - session'? But subs will also show up in other viewers and - finders. - - - list of recent resources that this choice was set to - """ - - def __init__(self, parent, graph, resourceObservable, label="Editing:"): - """ - getResource is called to get the URI of the currently - """ - self.graph = graph - self.frame = tk.Frame(parent, relief='raised', border=2) - self.frame.pack(side='top') - tk.Label(self.frame, text=label).pack(side='left') - self.currentLinkFrame = tk.Frame(self.frame) - self.currentLinkFrame.pack(side='left') - - self.subIcon = tk.Label(self.currentLinkFrame, - text="...", - borderwidth=2, - relief='raised', - padx=10, - pady=10) - self.subIcon.pack() - - self.resourceObservable = resourceObservable - resourceObservable.subscribe(self.uriChanged) - - # when the value is local, this should stop being a drag source - dragSourceRegister(self.subIcon, 'copy', 'text/uri-list', - self.resourceObservable) - - def onEv(ev): - self.resourceObservable(URIRef(ev.data)) - return "link" - - self.onEv = onEv - - b = tk.Button(self.frame, text="Unlink", command=self.switchToLocalSub) - b.pack(side='left') - - # it would be nice if I didn't receive my own drags here, and - # if the hover display wasn't per widget - for target in ([self.frame, self.currentLinkFrame] + - self.frame.winfo_children() + - self.currentLinkFrame.winfo_children()): - dropTargetRegister(target, - typeList=["*"], - onDrop=onEv, - hoverStyle=dict(background="#555500")) - - def uriChanged(self, newUri): - # if this resource had a type icon or a thumbnail, those would be - # cool to show in here too - if newUri is Local: - self.subIcon.config(text="(local)") - else: - self.graph.addHandler(self.updateLabel) - - def updateLabel(self): - uri = self.resourceObservable() - print("get label", repr(uri)) - label = self.graph.label(uri) - self.subIcon.config(text=label or uri) - - def switchToLocalSub(self): - self.resourceObservable(Local) diff -r 623836db99af -r 4556eebe5d73 light9/editchoicegtk.py --- a/light9/editchoicegtk.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,99 +0,0 @@ -import logging -from gi.repository import Gtk -from gi.repository import Gdk -from rdflib import URIRef -log = logging.getLogger('editchoicegtk') - - -class Local(object): - """placeholder for the local uri that EditChoice does not - manage. Set resourceObservable to Local to indicate that you're - unlinked""" - - -class EditChoice(Gtk.HBox): - """ - this is a gtk port of editchoice.EditChoice - """ - - def __init__(self, graph, resourceObservable, label="Editing:"): - """ - getResource is called to get the URI of the currently - """ - self.graph = graph - - # the outer box should have a distinctive border so it's more - # obviously a special drop target - Gtk.HBox.__init__(self) - self.pack_start(Gtk.Label(label), False, True, 0) #expand, fill, pad - - # this is just a label, but it should look like a physical - # 'thing' (and gtk labels don't work as drag sources) - self.currentLink = Gtk.Button("http://bar") - - self.pack_start(self.currentLink, True, True, 0) #expand, fill, pad - - self.unlinkButton = Gtk.Button(label="Unlink") - self.pack_start(self.unlinkButton, False, True, 0) #expand, fill pad - - self.unlinkButton.connect("clicked", self.onUnlink) - - self.show_all() - - self.resourceObservable = resourceObservable - resourceObservable.subscribe(self.uriChanged) - - self.makeDragSource() - self.makeDropTarget() - - def makeDropTarget(self): - - def ddr(widget, drag_context, x, y, selection_data, info, timestamp): - dtype = selection_data.get_data_type() - if dtype.name() not in ['text/uri-list', 'TEXT']: - raise ValueError("unknown DnD selection type %r" % dtype) - data = selection_data.get_data().strip() - log.debug('drag_data_received data=%r', data) - self.resourceObservable(URIRef(data)) - - self.currentLink.drag_dest_set( - flags=Gtk.DestDefaults.ALL, - targets=[ - Gtk.TargetEntry.new('text/uri-list', 0, 0), - Gtk.TargetEntry.new('TEXT', 0, - 0), # getting this from chrome :( - ], - actions=Gdk.DragAction.LINK | Gdk.DragAction.COPY) - self.currentLink.connect("drag_data_received", ddr) - - def makeDragSource(self): - self.currentLink.drag_source_set( - start_button_mask=Gdk.ModifierType.BUTTON1_MASK, - targets=[ - Gtk.TargetEntry.new(target='text/uri-list', flags=0, info=0) - ], - actions=Gdk.DragAction.LINK | Gdk.DragAction.COPY) - - def source_drag_data_get(btn, context, selection_data, info, time): - selection_data.set_uris([self.resourceObservable()]) - - self.currentLink.connect("drag_data_get", source_drag_data_get) - - def uriChanged(self, newUri): - # if this resource had a type icon or a thumbnail, those would be - # cool to show in here too - if newUri is Local: - self.currentLink.set_label("(local)") - self.currentLink.drag_source_unset() - else: - self.graph.addHandler(self.updateLabel) - self.makeDragSource() - self.unlinkButton.set_sensitive(newUri is not Local) - - def updateLabel(self): - uri = self.resourceObservable() - label = self.graph.label(uri) - self.currentLink.set_label(label or uri or "") - - def onUnlink(self, *args): - self.resourceObservable(Local) diff -r 623836db99af -r 4556eebe5d73 light9/effect/__init__.py diff -r 623836db99af -r 4556eebe5d73 light9/effect/edit.py --- a/light9/effect/edit.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,205 +0,0 @@ -from rdflib import URIRef, Literal -from twisted.internet.defer import inlineCallbacks, returnValue -import treq - -from light9 import networking -from light9.curvecalc.curve import CurveResource -from light9.namespaces import L9, RDF, RDFS -from rdfdb.patch import Patch - - -def clamp(x, lo, hi): - return max(lo, min(hi, x)) - - -@inlineCallbacks -def getMusicStatus(): - resp = yield treq.get(networking.musicPlayer.path('time'), timeout=.5) - body = yield resp.json_content() - returnValue(body) - - -@inlineCallbacks -def songEffectPatch(graph, dropped, song, event, ctx): - """ - some uri was 'dropped' in the curvecalc timeline. event is 'default' or 'start' or 'end'. - """ - with graph.currentState(tripleFilter=(dropped, None, None)) as g: - droppedTypes = list(g.objects(dropped, RDF.type)) - droppedLabel = g.label(dropped) - droppedCodes = list(g.objects(dropped, L9['code'])) - - quads = [] - fade = 2 if event == 'default' else 0 - - if _songHasEffect(graph, song, dropped): - # bump the existing curve - pass - else: - effect, q = _newEffect(graph, song, ctx) - quads.extend(q) - - curve = graph.sequentialUri(song + "/curve-") - yield _newEnvelopeCurve(graph, ctx, curve, droppedLabel, fade) - quads.extend([ - (song, L9['curve'], curve, ctx), - (effect, RDFS.label, droppedLabel, ctx), - (effect, L9['code'], Literal('env = %s' % curve.n3()), ctx), - ]) - - if L9['EffectClass'] in droppedTypes: - quads.extend([ - (effect, RDF.type, dropped, ctx), - ] + [(effect, L9['code'], c, ctx) for c in droppedCodes]) - elif L9['Submaster'] in droppedTypes: - quads.extend([ - (effect, L9['code'], Literal('out = %s * env' % dropped.n3()), - ctx), - ]) - else: - raise NotImplementedError( - "don't know how to add an effect from %r (types=%r)" % - (dropped, droppedTypes)) - - _maybeAddMusicLine(quads, effect, song, ctx) - - print("adding") - for qq in quads: - print(qq) - returnValue(Patch(addQuads=quads)) - - -@inlineCallbacks -def songNotePatch(graph, dropped, song, event, ctx, note=None): - """ - drop into effectsequencer timeline - - ported from timeline.coffee makeNewNote - """ - with graph.currentState(tripleFilter=(dropped, None, None)) as g: - droppedTypes = list(g.objects(dropped, RDF.type)) - - quads = [] - fade = 2 if event == 'default' else 0.1 - - if note: - musicStatus = yield getMusicStatus() - songTime = musicStatus['t'] - _finishCurve(graph, note, quads, ctx, songTime) - else: - if L9['Effect'] in droppedTypes: - musicStatus = yield getMusicStatus() - songTime = musicStatus['t'] - note = _makeNote(graph, song, note, quads, ctx, dropped, songTime, - event, fade) - else: - raise NotImplementedError - - returnValue((note, Patch(addQuads=quads))) - - -def _point(ctx, uri, t, v): - return [(uri, L9['time'], Literal(round(t, 3)), ctx), - (uri, L9['value'], Literal(round(v, 3)), ctx)] - - -def _finishCurve(graph, note, quads, ctx, songTime): - with graph.currentState() as g: - origin = g.value(note, L9['originTime']).toPython() - curve = g.value(note, L9['curve']) - - pt2 = graph.sequentialUri(curve + 'p') - pt3 = graph.sequentialUri(curve + 'p') - quads.extend([(curve, L9['point'], pt2, ctx)] + - _point(ctx, pt2, songTime - origin, 1) + - [(curve, L9['point'], pt3, ctx)] + - _point(ctx, pt3, songTime - origin + .5, 0)) - - -def _makeNote(graph, song, note, quads, ctx, dropped, songTime, event, fade): - note = graph.sequentialUri(song + '/n') - curve = graph.sequentialUri(note + 'c') - quads.extend([ - (song, L9['note'], note, ctx), - (note, RDF.type, L9['Note'], ctx), - (note, L9['curve'], curve, ctx), - (note, L9['effectClass'], dropped, ctx), - (note, L9['originTime'], Literal(songTime), ctx), - (curve, RDF.type, L9['Curve'], ctx), - (curve, L9['attr'], L9['strength'], ctx), - ]) - if event == 'default': - coords = [(0 - fade, 0), (0, 1), (20, 1), (20 + fade, 0)] - elif event == 'start': - coords = [ - (0 - fade, 0), - (0, 1), - ] - elif event == 'end': # probably unused- goes to _finishCurve instead - coords = [(20, 1), (20 + fade, 0)] - else: - raise NotImplementedError(event) - for t, v in coords: - pt = graph.sequentialUri(curve + 'p') - quads.extend([(curve, L9['point'], pt, ctx)] + _point(ctx, pt, t, v)) - return note - - -def _songHasEffect(graph, song, uri): - """does this song have an effect of class uri or a sub curve for sub - uri? this should be simpler to look up.""" - return False # todo - - -def musicCurveForSong(uri): - return URIRef(uri + 'music') - - -def _newEffect(graph, song, ctx): - effect = graph.sequentialUri(song + "/effect-") - quads = [ - (song, L9['effect'], effect, ctx), - (effect, RDF.type, L9['Effect'], ctx), - ] - print("_newEffect", effect, quads) - return effect, quads - - -@inlineCallbacks -def _newEnvelopeCurve(graph, ctx, uri, label, fade=2): - """this does its own patch to the graph""" - - cr = CurveResource(graph, uri) - cr.newCurve(ctx, label=Literal(label)) - yield _insertEnvelopePoints(cr.curve, fade) - cr.saveCurve() - - -@inlineCallbacks -def _insertEnvelopePoints(curve, fade=2): - # wrong: we might not be adding to the currently-playing song. - musicStatus = yield getMusicStatus() - songTime = musicStatus['t'] - songDuration = musicStatus['duration'] - - t1 = clamp(songTime - fade, .1, songDuration - .1 * 2) + fade - t2 = clamp(songTime + 20, t1 + .1, songDuration) - - curve.insert_pt((t1 - fade, 0)) - curve.insert_pt((t1, 1)) - curve.insert_pt((t2, 1)) - curve.insert_pt((t2 + fade, 0)) - - -def _maybeAddMusicLine(quads, effect, song, ctx): - """ - add a line getting the current music into 'music' if any code might - be mentioning that var - """ - - for spoc in quads: - if spoc[1] == L9['code'] and 'music' in spoc[2]: - quads.extend([(effect, L9['code'], - Literal('music = %s' % musicCurveForSong(song).n3()), - ctx)]) - break diff -r 623836db99af -r 4556eebe5d73 light9/effect/effect_function_library.py --- a/light9/effect/effect_function_library.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,67 +0,0 @@ -"""repo of the EffectFunctions in the graph. Includes URI->realPythonFunction""" -import logging -from dataclasses import dataclass, field -from typing import Callable, List, Optional, cast - -from rdfdb.syncedgraph.syncedgraph import SyncedGraph -from rdflib import RDF, RDFS, Literal - -from light9.namespaces import FUNC, L9 -from light9.newtypes import EffectAttr, EffectFunction, VTUnion -from light9.typedgraph import typedValue - -from . import effect_functions - -log = logging.getLogger('effectfuncs') - - -@dataclass -class _EffectFunctionInput: - effectAttr: EffectAttr - defaultValue: Optional[VTUnion] - - -@dataclass -class _RdfEffectFunction: - uri: EffectFunction - label: Optional[Literal] - inputs: List[_EffectFunctionInput] - - -@dataclass -class EffectFunctionLibrary: - """parses :EffectFunction structures""" - graph: SyncedGraph - - funcs: List[_RdfEffectFunction] = field(default_factory=list) - - def __post_init__(self): - self.graph.addHandler(self._compile) - - def _compile(self): - self.funcs = [] - for subj in self.graph.subjects(RDF.type, L9['EffectFunction']): - label = typedValue(Literal | None, self.graph, subj, RDFS.label) - inputs = [] - for inp in self.graph.objects(subj, L9['input']): - inputs.append( - _EffectFunctionInput( # - typedValue(EffectAttr, self.graph, inp, L9['effectAttr']), # - typedValue(VTUnion | None, self.graph, inp, L9['defaultValue']))) - - self.funcs.append(_RdfEffectFunction(cast(EffectFunction, subj), label, inputs)) - - def getFunc(self, uri: EffectFunction) -> Callable: - return { - FUNC['scale']: effect_functions.effect_scale, - FUNC['strobe']: effect_functions.effect_strobe, - }[uri] - - def getDefaultValue(self, uri: EffectFunction, attr: EffectAttr) -> VTUnion: - for f in self.funcs: - if f.uri == uri: - for i in f.inputs: - if i.effectAttr == attr: - if i.defaultValue is not None: - return i.defaultValue - raise ValueError(f'no default for {uri} {attr}') \ No newline at end of file diff -r 623836db99af -r 4556eebe5d73 light9/effect/effect_function_library_test.py --- a/light9/effect/effect_function_library_test.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,38 +0,0 @@ -from light9.effect.effect_function_library import EffectFunctionLibrary -from light9.mock_syncedgraph import MockSyncedGraph - -PREFIXES = ''' -@prefix : . -@prefix dev: . -@prefix effect: . -@prefix func: . -@prefix rdfs: . -@prefix xsd: . -''' - -GRAPH = PREFIXES + ''' - - func:scale - a :EffectFunction; - rdfs:label "a submaster- scales :deviceSettings"; - :input - [ :effectAttr :strength; :defaultValue 0.0 ], - [ :effectAttr :deviceSettings; ] . # e.g. "par2 at color=red; par3 at color=white" - - func:strobe - a :EffectFunction; - rdfs:label "blink specified devices"; - :input - [ :effectAttr :strength; :defaultValue 0.0 ], - [ :effectAttr :period; :defaultValue 0.5 ], - [ :effectAttr :onTime; :defaultValue 0.1 ], - [ :effectAttr :deviceSettings ] . -''' - - -class TestParsesGraph: - - def test(self): - g = MockSyncedGraph(GRAPH) - lib = EffectFunctionLibrary(g) - assert len(lib.funcs) == 2 \ No newline at end of file diff -r 623836db99af -r 4556eebe5d73 light9/effect/effect_functions.py --- a/light9/effect/effect_functions.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,63 +0,0 @@ -import logging -import random - -from PIL import Image -from webcolors import rgb_to_hex - -from light9.effect.scale import scale -from light9.effect.settings import DeviceSettings -from light9.namespaces import L9 - -random.seed(0) - -log = logging.getLogger('effectfunc') - - -def sample8(img, x, y, repeat=False): - if not (0 <= y < img.height): - return (0, 0, 0) - if 0 <= x < img.width: - return img.getpixel((x, y)) - elif not repeat: - return (0, 0, 0) - else: - return img.getpixel((x % img.width, y)) - - -def effect_scale(strength: float, devs: DeviceSettings) -> DeviceSettings: - out = [] - if strength != 0: - for d, da, v in devs.asList(): - out.append((d, da, scale(v, strength))) - return DeviceSettings(devs.graph, out) - - -def effect_strobe( - songTime: float, # - strength: float, - period: float, - onTime: float, - devs: DeviceSettings) -> DeviceSettings: - if period == 0: - scl = 0 - else: - scl = strength if (songTime % period) < onTime else 0 - return effect_scale(scl, devs) - - -def effect_image( - songTime: float, # - strength: float, - period: float, - image: Image.Image, - devs: DeviceSettings, -) -> DeviceSettings: - x = int((songTime / period) * image.width) - out = [] - for y, (d, da, v) in enumerate(devs.asOrderedList()): - if da != L9['color']: - continue - color8 = sample8(image, x, y, repeat=True) - color = rgb_to_hex(tuple(color8)) - out.append((d, da, scale(color, strength * v))) - return DeviceSettings(devs.graph, out) \ No newline at end of file diff -r 623836db99af -r 4556eebe5d73 light9/effect/effecteval.py --- a/light9/effect/effecteval.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,466 +0,0 @@ -import logging -import math -import random -from colorsys import hsv_to_rgb - -from noise import pnoise1 -from PIL import Image -from rdflib import Literal, Namespace -from webcolors import hex_to_rgb, rgb_to_hex - -from light9.effect.scale import scale -from light9.namespaces import DEV, L9 - -SKY = Namespace('http://light9.bigasterisk.com/theater/skyline/device/') - -random.seed(0) - -log = logging.getLogger('effecteval') -log.info("reload effecteval") - - -def literalColor(rnorm, gnorm, bnorm): - return Literal(rgb_to_hex(( - int(rnorm * 255), # - int(gnorm * 255), # - int(bnorm * 255)))) - - -def literalColorHsv(h, s, v): - return literalColor(*hsv_to_rgb(h, s, v)) - - -def nsin(x): - return (math.sin(x * (2 * math.pi)) + 1) / 2 - - -def ncos(x): - return (math.cos(x * (2 * math.pi)) + 1) / 2 - - -def nsquare(t, on=.5): - return (t % 1.0) < on - - -def lerp(a, b, t): - return a + (b - a) * t - - -def noise(t): - return pnoise1(t % 1000.0, 2) - - -def clamp(lo, hi, x): - return max(lo, min(hi, x)) - - -def clamp255(x): - return min(255, max(0, x)) - - -def _8bit(f): - if not isinstance(f, (int, float)): - raise TypeError(repr(f)) - return clamp255(int(f * 255)) - - -def effect_Curtain(effectSettings, strength, songTime, noteTime): - return {(L9['device/lowPattern%s' % n], L9['color']): literalColor(strength, strength, strength) for n in range(301, 308 + 1)} - - -def effect_animRainbow(effectSettings, strength, songTime, noteTime): - out = {} - tint = effectSettings.get(L9['tint'], '#ffffff') - tintStrength = float(effectSettings.get(L9['tintStrength'], 0)) - tr, tg, tb = hex_to_rgb(tint) - for n in range(1, 5 + 1): - scl = strength * nsin(songTime + n * .3)**3 - col = literalColor(scl * lerp(nsin(songTime + n * .2), tr / 255, tintStrength), scl * lerp(nsin(songTime + n * .2 + .3), tg / 255, tintStrength), - scl * lerp(nsin(songTime + n * .3 + .6), tb / 255, tintStrength)) - - dev = L9['device/aura%s' % n] - out.update({ - (dev, L9['color']): col, - (dev, L9['zoom']): .9, - }) - ang = songTime * 4 - out.update({ - (dev, L9['rx']): lerp(.27, .7, (n - 1) / 4) + .2 * math.sin(ang + n), - (dev, L9['ry']): lerp(.46, .52, (n - 1) / 4) + .5 * math.cos(ang + n), - }) - return out - - -def effect_auraSparkles(effectSettings, strength, songTime, noteTime): - out = {} - tint = effectSettings.get(L9['tint'], '#ffffff') - print(effectSettings) - tr, tg, tb = hex_to_rgb(tint) - for n in range(1, 5 + 1): - scl = strength * ((int(songTime * 10) % n) < 1) - col = literalColorHsv((songTime + (n / 5)) % 1, 1, scl) - - dev = L9['device/aura%s' % n] - out.update({ - (dev, L9['color']): col, - (dev, L9['zoom']): .95, - }) - ang = songTime * 4 - out.update({ - (dev, L9['rx']): lerp(.27, .8, (n - 1) / 4) + .2 * math.sin(ang + n), - (dev, L9['ry']): lerp(.46, .52, (n - 1) / 4) + .4 * math.cos(ang + n), - }) - return out - - -def effect_qpan(effectSettings, strength, songTime, noteTime): - dev = L9['device/q2'] - dur = 4 - col = scale(scale('#ffffff', strength), effectSettings.get(L9['colorScale']) or '#ffffff') - return { - (dev, L9['color']): col, - (dev, L9['focus']): 0.589, - (dev, L9['rx']): lerp(0.778, 0.291, clamp(0, 1, noteTime / dur)), - (dev, L9['ry']): 0.5, - (dev, L9['zoom']): 0.714, - } - - -def effect_pulseRainbow(effectSettings, strength, songTime, noteTime): - out = {} - tint = effectSettings.get(L9['tint'], '#ffffff') - tintStrength = float(effectSettings.get(L9['tintStrength'], 0)) - tr, tg, tb = hex_to_rgb(tint) - for n in range(1, 5 + 1): - scl = strength - col = literalColor(scl * lerp(nsin(songTime + n * .2), tr / 255, tintStrength), scl * lerp(nsin(songTime + n * .2 + .3), tg / 255, tintStrength), - scl * lerp(nsin(songTime + n * .3 + .6), tb / 255, tintStrength)) - - dev = L9['device/aura%s' % n] - out.update({ - (dev, L9['color']): col, - (dev, L9['zoom']): .5, - }) - out.update({ - (dev, L9['rx']): lerp(.27, .7, (n - 1) / 4), - (dev, L9['ry']): lerp(.46, .52, (n - 1) / 4), - }) - return out - - -def effect_aurawash(effectSettings, strength, songTime, noteTime): - out = {} - scl = strength - period = float(effectSettings.get(L9['period'], 125 / 60 / 4)) - if period < .05: - quantTime = songTime - else: - quantTime = int(songTime / period) * period - noisePos = quantTime * 6.3456 - - col = literalColorHsv(noise(noisePos), 1, scl) - col = scale(col, effectSettings.get(L9['colorScale']) or '#ffffff') - - print(songTime, quantTime, col) - - for n in range(1, 5 + 1): - dev = L9['device/aura%s' % n] - out.update({ - (dev, L9['color']): col, - (dev, L9['zoom']): .5, - }) - out.update({ - (dev, L9['rx']): lerp(.27, .7, (n - 1) / 4), - (dev, L9['ry']): lerp(.46, .52, (n - 1) / 4), - }) - return out - - -def effect_qsweep(effectSettings, strength, songTime, noteTime): - out = {} - period = float(effectSettings.get(L9['period'], 2)) - - col = effectSettings.get(L9['colorScale'], '#ffffff') - col = scale(col, effectSettings.get(L9['strength'], 1)) - - for n in range(1, 3 + 1): - dev = L9['device/q%s' % n] - out.update({ - (dev, L9['color']): col, - (dev, L9['zoom']): effectSettings.get(L9['zoom'], .5), - }) - out.update({ - (dev, L9['rx']): lerp(.3, .8, nsin(songTime / period + n / 4)), - (dev, L9['ry']): effectSettings.get(L9['ry'], .2), - }) - return out - - -def effect_qsweepusa(effectSettings, strength, songTime, noteTime): - out = {} - period = float(effectSettings.get(L9['period'], 2)) - - colmap = { - 1: '#ff0000', - 2: '#998888', - 3: '#0050ff', - } - - for n in range(1, 3 + 1): - dev = L9['device/q%s' % n] - out.update({ - (dev, L9['color']): scale(colmap[n], effectSettings.get(L9['strength'], 1)), - (dev, L9['zoom']): effectSettings.get(L9['zoom'], .5), - }) - out.update({ - (dev, L9['rx']): lerp(.3, .8, nsin(songTime / period + n / 4)), - (dev, L9['ry']): effectSettings.get(L9['ry'], .5), - }) - return out - - -chase1_members = [ - DEV['backlight1'], - DEV['lip1'], - DEV['backlight2'], - DEV['down2'], - DEV['lip2'], - DEV['backlight3'], - DEV['down3'], - DEV['lip3'], - DEV['backlight4'], - DEV['down4'], - DEV['lip4'], - DEV['backlight5'], - DEV['down5Edge'], - DEV['lip5'], - #DEV['upCenter'], -] -chase2_members = chase1_members * 10 -random.shuffle(chase2_members) - - -def effect_chase1(effectSettings, strength, songTime, noteTime): - members = chase1_members + chase1_members[-2:0:-1] - - out = {} - period = float(effectSettings.get(L9['period'], 2 / len(members))) - - for i, dev in enumerate(members): - cursor = (songTime / period) % float(len(members)) - dist = abs(i - cursor) - radius = 3 - if dist < radius: - col = effectSettings.get(L9['colorScale'], '#ffffff') - col = scale(col, effectSettings.get(L9['strength'], 1)) - col = scale(col, (1 - dist / radius)) - - out.update({ - (dev, L9['color']): col, - }) - return out - - -def effect_chase2(effectSettings, strength, songTime, noteTime): - members = chase2_members - - out = {} - period = float(effectSettings.get(L9['period'], 0.3)) - - for i, dev in enumerate(members): - cursor = (songTime / period) % float(len(members)) - dist = abs(i - cursor) - radius = 3 - if dist < radius: - col = effectSettings.get(L9['colorScale'], '#ffffff') - col = scale(col, effectSettings.get(L9['strength'], 1)) - col = scale(col, (1 - dist / radius)) - - out.update({ - (dev, L9['color']): col, - }) - return out - - -def effect_whirlscolor(effectSettings, strength, songTime, noteTime): - out = {} - - col = effectSettings.get(L9['colorScale'], '#ffffff') - col = scale(col, effectSettings.get(L9['strength'], 1)) - - for n in (1, 3): - dev = L9['device/q%s' % n] - scl = strength - col = literalColorHsv(((songTime / 5) + (n / 5)) % 1, 1, scl) - out.update({ - (dev, L9['color']): col, - }) - - return out - - -def effect_orangeSearch(effectSettings, strength, songTime, noteTime): - dev = L9['device/auraStage'] - return { - (dev, L9['color']): '#a885ff', - (dev, L9['rx']): lerp(.65, 1, nsin(songTime / 2.0)), - (dev, L9['ry']): .6, - (dev, L9['zoom']): 1, - } - - -def effect_Strobe(effectSettings, strength, songTime, noteTime): - rate = 2 - duty = .3 - offset = 0 - f = (((songTime + offset) * rate) % 1.0) - c = (f < duty) * strength - col = rgb_to_hex((int(c * 255), int(c * 255), int(c * 255))) - return {(L9['device/colorStrip'], L9['color']): Literal(col)} - - -def effect_lightning(effectSettings, strength, songTime, noteTime): - devs = [ - L9['device/veryLow1'], L9['device/veryLow2'], L9['device/veryLow3'], L9['device/veryLow4'], L9['device/veryLow5'], L9['device/backlight1'], - L9['device/backlight2'], L9['device/backlight3'], L9['device/backlight4'], L9['device/backlight5'], L9['device/down2'], L9['device/down3'], - L9['device/down4'], L9['device/hexLow3'], L9['device/hexLow5'], L9['device/postL1'], L9['device/postR1'] - ] - out = {} - col = rgb_to_hex((int(255 * strength),) * 3) - for i, dev in enumerate(devs): - n = noise(songTime * 8 + i * 6.543) - if n > .4: - out[(dev, L9['color'])] = col - return out - - -def sample8(img, x, y, repeat=False): - if not (0 <= y < img.height): - return (0, 0, 0) - if 0 <= x < img.width: - return img.getpixel((x, y)) - elif not repeat: - return (0, 0, 0) - else: - return img.getpixel((x % img.width, y)) - - -def effect_image(effectSettings, strength, songTime, noteTime): - out = {} - imageSetting = effectSettings.get(L9["image"], 'specks.png') - imgPath = f'cur/anim/{imageSetting}' - t_offset = effectSettings.get(L9['tOffset'], 0) - pxPerSec = effectSettings.get(L9['pxPerSec'], 30) - img = Image.open(imgPath) - x = (noteTime * pxPerSec) - - colorScale = hex_to_rgb(effectSettings.get(L9['colorScale'], '#ffffff')) - - for dev, y in [ - (SKY['strip1'], 0), - (SKY['strip2'], 1), - (SKY['strip3'], 2), - (SKY['par3'], 3), # dl - (SKY['par4'], 4), # ul - (SKY['par7'], 5), # ur - (SKY['par1'], 6), # dr - ('cyc1', 7), - ('cyc2', 8), - ('cyc3', 9), - ('cyc4', 10), - ('down1', 11), - ('down2', 12), - ('down3', 13), - ('down4', 14), - ('down5', 15), - ('down6', 16), - ('down7', 17), - ]: - color8 = sample8(img, x, y, effectSettings.get(L9['repeat'], True)) - color = map(lambda v: v / 255 * strength, color8) - color = [v * cs / 255 for v, cs in zip(color, colorScale)] - if dev in ['cyc1', 'cyc2', 'cyc3', 'cyc4']: - column = dev[-1] - out[(SKY[f'cycRed{column}'], L9['brightness'])] = color[0] - out[(SKY[f'cycGreen{column}'], L9['brightness'])] = color[1] - out[(SKY[f'cycBlue{column}'], L9['brightness'])] = color[2] - else: - out[(dev, L9['color'])] = rgb_to_hex(tuple(map(_8bit, color))) - return out - - -def effect_cyc(effectSettings, strength, songTime, noteTime): - colorScale = effectSettings.get(L9['colorScale'], '#ffffff') - r, g, b = map(lambda x: strength * x / 255, hex_to_rgb(colorScale)) - - out = { - (SKY['cycRed1'], L9['brightness']): r, - (SKY['cycRed2'], L9['brightness']): r, - (SKY['cycRed3'], L9['brightness']): r, - (SKY['cycRed4'], L9['brightness']): r, - (SKY['cycGreen1'], L9['brightness']): g, - (SKY['cycGreen2'], L9['brightness']): g, - (SKY['cycGreen3'], L9['brightness']): g, - (SKY['cycGreen4'], L9['brightness']): g, - (SKY['cycBlue1'], L9['brightness']): b, - (SKY['cycBlue2'], L9['brightness']): b, - (SKY['cycBlue3'], L9['brightness']): b, - (SKY['cycBlue4'], L9['brightness']): b, - } - - return out - - -cycChase1_members = [ - SKY['cycRed1'], - SKY['cycRed2'], - SKY['cycRed3'], - SKY['cycRed4'], - SKY['cycGreen1'], - SKY['cycGreen2'], - SKY['cycGreen3'], - SKY['cycGreen4'], - SKY['cycBlue1'], - SKY['cycBlue2'], - SKY['cycBlue3'], - SKY['cycBlue4'], -] -cycChase1_members = cycChase1_members * 20 -random.shuffle(cycChase1_members) - - -def effect_cycChase1(effectSettings, strength, songTime, noteTime): - colorScale = effectSettings.get(L9['colorScale'], '#ffffff') - r, g, b = map(lambda x: x / 255, hex_to_rgb(colorScale)) - tintAmount = {'Red': r, 'Green': g, 'Blue': b} - - members = cycChase1_members - - out = {} - period = float(effectSettings.get(L9['period'], 6 / len(members))) - - for i, dev in enumerate(members): - cursor = (songTime / period) % float(len(members)) - dist = abs(i - cursor) - radius = 7 - if dist < radius: - colorFromUri = str(dev).split('/')[-1].split('cyc')[1][:-1] - scale = strength * tintAmount[colorFromUri] - out.update({ - (dev, L9['brightness']): (1 - dist / radius) * scale, - }) - return out - - -def effect_parNoise(effectSettings, strength, songTime, noteTime): - colorScale = effectSettings.get(L9['colorScale'], '#ffffff') - r, g, b = map(lambda x: x / 255, hex_to_rgb(colorScale)) - out = {} - speed = 10 - gamma = .6 - for dev in [SKY['strip1'], SKY['strip2'], SKY['strip3']]: - out[(dev, L9['color'])] = scale( - rgb_to_hex((_8bit(r * math.pow(max(.01, noise(speed * songTime)), gamma)), _8bit(g * math.pow(max(.01, noise(speed * songTime + 10)), gamma)), - _8bit(b * math.pow(max(.01, noise(speed * songTime + 20)), gamma)))), strength) - - return out diff -r 623836db99af -r 4556eebe5d73 light9/effect/effecteval2.py --- a/light9/effect/effecteval2.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,122 +0,0 @@ -import traceback -import inspect -import logging -from dataclasses import dataclass -from typing import Callable, List, Optional - -from rdfdb.syncedgraph.syncedgraph import SyncedGraph -from rdflib import RDF -from rdflib.term import Node - -from light9.effect.effect_function_library import EffectFunctionLibrary -from light9.effect.settings import DeviceSettings, EffectSettings -from light9.namespaces import L9 -from light9.newtypes import (DeviceAttr, DeviceUri, EffectAttr, EffectFunction, EffectUri, VTUnion) -from light9.typedgraph import typedValue - -log = logging.getLogger('effecteval') - - -@dataclass -class Config: - effectFunction: EffectFunction - esettings: EffectSettings - devSettings: Optional[DeviceSettings] # the EffectSettings :effectAttr :devSettings item, if there was one - func: Callable - funcArgs: List[inspect.Parameter] - - -@dataclass -class EffectEval2: - """Runs one effect code to turn EffectSettings (e.g. strength) into DeviceSettings""" - graph: SyncedGraph - uri: EffectUri - lib: EffectFunctionLibrary - - config: Optional[Config] = None - - def __post_init__(self): - self.graph.addHandler(self._compile) - - def _compile(self): - self.config = None - if not self.graph.contains((self.uri, RDF.type, L9['Effect'])): - return - - try: - effectFunction = typedValue(EffectFunction, self.graph, self.uri, L9['effectFunction']) - effSets = [] - devSettings = None - for s in self.graph.objects(self.uri, L9['setting']): - attr = typedValue(EffectAttr, self.graph, s, L9['effectAttr']) - if attr == L9['deviceSettings']: - value = typedValue(Node, self.graph, s, L9['value']) - - rows = [] - for ds in self.graph.objects(value, L9['setting']): - d = typedValue(DeviceUri, self.graph, ds, L9['device']) - da = typedValue(DeviceAttr, self.graph, ds, L9['deviceAttr']) - v = typedValue(VTUnion, self.graph, ds, L9['value']) - rows.append((d, da, v)) - devSettings = DeviceSettings(self.graph, rows) - else: - value = typedValue(VTUnion, self.graph, s, L9['value']) - effSets.append((self.uri, attr, value)) - esettings = EffectSettings(self.graph, effSets) - - try: - effectFunction = typedValue(EffectFunction, self.graph, self.uri, L9['effectFunction']) - except ValueError: - raise ValueError(f'{self.uri} has no :effectFunction') - func = self.lib.getFunc(effectFunction) - - # This should be in EffectFunctionLibrary - funcArgs = list(inspect.signature(func).parameters.values()) - - self.config = Config(effectFunction, esettings, devSettings, func, funcArgs) - except Exception: - log.error(f"while compiling {self.uri}") - traceback.print_exc() - - def compute(self, songTime: float, inputs: EffectSettings) -> DeviceSettings: - """ - calls our function using inputs (publishedAttr attrs, e.g. :strength) - and effect-level settings including a special attr called :deviceSettings - with DeviceSettings as its value - """ - if self.config is None: - return DeviceSettings(self.graph, []) - - c = self.config - kw = {} - for arg in c.funcArgs: - if arg.annotation == DeviceSettings: - v = c.devSettings - if v is None: # asked for ds but we have none - log.debug("%s asked for devs but we have none in config", self.uri) - return DeviceSettings(self.graph, []) - elif arg.name == 'songTime': - v = songTime - else: - eaForName = EffectAttr(L9[arg.name]) - v = self._getEffectAttrValue(eaForName, inputs) - - kw[arg.name] = v - - if False and log.isEnabledFor(logging.DEBUG): - log.debug('calling %s with %s', c.func, kw) - return c.func(**kw) - - def _getEffectAttrValue(self, attr: EffectAttr, inputs: EffectSettings) -> VTUnion: - c = self.config - if c is None: - raise - try: - return inputs.getValue(self.uri, attr, defaultToZero=False) - except KeyError: - pass - try: - return c.esettings.getValue(self.uri, attr, defaultToZero=False) - except KeyError: - pass - return self.lib.getDefaultValue(c.effectFunction, attr) diff -r 623836db99af -r 4556eebe5d73 light9/effect/effecteval_test.py --- a/light9/effect/effecteval_test.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,123 +0,0 @@ -from typing import List, Tuple - -import pytest - -from light9.effect.effect_function_library import EffectFunctionLibrary -from light9.effect.effecteval2 import EffectEval2 -from light9.effect.settings import DeviceSettings, EffectSettings -from light9.mock_syncedgraph import MockSyncedGraph -from light9.namespaces import DEV, L9 -from light9.newtypes import (DeviceAttr, DeviceUri, EffectAttr, EffectUri, HexColor, VTUnion) - -PREFIX = ''' - @prefix : . - @prefix dev: . - @prefix effect: . - @prefix func: . - @prefix rdfs: . -''' - -GRAPH = PREFIX + ''' - - func:scale - a :EffectFunction; - rdfs:label "a submaster- scales :deviceSettings"; - :input - [ :effectAttr :strength; :defaultValue 0.0 ], - [ :effectAttr :deviceSettings; ] . # e.g. "par2 at color=red; par3 at color=white" - - func:strobe - a :EffectFunction; - rdfs:label "blink specified devices"; - :input - [ :effectAttr :strength; :defaultValue 0.0 ], - [ :effectAttr :period; :defaultValue 0.5 ], - [ :effectAttr :onTime; :defaultValue 0.1 ], - [ :effectAttr :deviceSettings ] . - - func:image - a :EffectFunction; - rdfs:label "sample image at x=time"; - :input - [ :effectAttr :strength; :defaultValue 0.0 ], - [ :effectAttr :period; :defaultValue 2.0 ], - [ :effectAttr :image; :defaultValue "specks.png" ], - [ :effectAttr :deviceSettings; rdfs:comment "these might have a :sort key or a :y value" ] . - - - :effectSub - a :Effect; - :effectFunction func:scale; - :publishAttr :strength; - :setting [ :effectAttr :deviceSettings; :value [ - :setting [ :device dev:light1; :deviceAttr :color; :value "#ff0000" ] ] ]. - - :effectDefaultStrobe - a :Effect; - :effectFunction func:strobe; - :publishAttr :strength; - :setting [ :effectAttr :deviceSettings; :value [ - :setting [ :device dev:light1; :deviceAttr :color; :value "#ff0000" ] ] ]. - - :effectCustomStrobe - a :Effect; - :effectFunction func:strobe; - :publishAttr :strength; - :setting - [ :effectAttr :period; :value 3.0], - [ :effectAttr :onTime; :value 0.5], - [ :effectAttr :deviceSettings; :value [ - :setting [ :device dev:light1; :deviceAttr :color; :value "#ff0000" ] ] ]. -''' - -effectSub = EffectUri(L9['effectSub']) -effectDefaultStrobe = EffectUri(L9['effectDefaultStrobe']) -effectCustomStrobe = EffectUri(L9['effectCustomStrobe']) - - -def light1At(col: str) -> List[Tuple[DeviceUri, DeviceAttr, VTUnion]]: - return [(DeviceUri(DEV['light1']), DeviceAttr(L9['color']), HexColor(col))] - - -@pytest.fixture -def effectFunctions(): - g = MockSyncedGraph(GRAPH) - return EffectFunctionLibrary(g) - - -class TestEffectEval: - - def test_scalesColors(self, effectFunctions): - g = effectFunctions.graph - ee = EffectEval2(g, effectSub, effectFunctions) - s = EffectSettings(g, [(effectSub, EffectAttr(L9['strength']), 0.5)]) - ds = ee.compute(songTime=0, inputs=s) - assert ds == DeviceSettings(g, light1At('#7f0000')) - - def test_cullsZeroOutputs(self, effectFunctions): - g = effectFunctions.graph - ee = EffectEval2(g, effectSub, effectFunctions) - s = EffectSettings(g, [(effectSub, EffectAttr(L9['strength']), 0.0)]) - ds = ee.compute(songTime=0, inputs=s) - assert ds == DeviceSettings(g, []) - - def test_strobeDefaults(self, effectFunctions): - g = effectFunctions.graph - ee = EffectEval2(g, effectDefaultStrobe, effectFunctions) - s = EffectSettings(g, [(effectDefaultStrobe, EffectAttr(L9['strength']), 1.0)]) - assert ee.compute(songTime=0, inputs=s) == DeviceSettings(g, light1At('#ff0000')) - assert ee.compute(songTime=.25, inputs=s) == DeviceSettings(g, []) - - def strobeMultsStrength(self, effectFunctions): - g = effectFunctions.graph - ee = EffectEval2(g, effectDefaultStrobe, effectFunctions) - s = EffectSettings(g, [(effectDefaultStrobe, EffectAttr(L9['strength']), 0.5)]) - assert ee.compute(songTime=0, inputs=s) == DeviceSettings(g, light1At('#7f0000')) - - def test_strobeCustom(self, effectFunctions): - g = effectFunctions.graph - ee = EffectEval2(g, effectCustomStrobe, effectFunctions) - s = EffectSettings(g, [(effectCustomStrobe, EffectAttr(L9['strength']), 1.0)]) - assert ee.compute(songTime=0, inputs=s) == DeviceSettings(g, light1At('#ff0000')) - assert ee.compute(songTime=.25, inputs=s) == DeviceSettings(g, light1At('#ff0000')) - assert ee.compute(songTime=.6, inputs=s) == DeviceSettings(g, []) diff -r 623836db99af -r 4556eebe5d73 light9/effect/scale.py --- a/light9/effect/scale.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,27 +0,0 @@ -from decimal import Decimal - -from webcolors import hex_to_rgb, rgb_to_hex - -from light9.newtypes import VTUnion - - -def scale(value: VTUnion, strength: float): - if isinstance(value, Decimal): - raise TypeError() - - if isinstance(value, str): - if value[0] == '#': - if strength == '#ffffff': - return value - r, g, b = hex_to_rgb(value) - # if isinstance(strength, Literal): - # strength = strength.toPython() - # if isinstance(strength, str): - # sr, sg, sb = [v / 255 for v in hex_to_rgb(strength)] - if True: - sr = sg = sb = strength - return rgb_to_hex((int(r * sr), int(g * sg), int(b * sb))) - elif isinstance(value, (int, float)): - return value * strength - - raise NotImplementedError("%r,%r" % (value, strength)) diff -r 623836db99af -r 4556eebe5d73 light9/effect/sequencer/__init__.py --- a/light9/effect/sequencer/__init__.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -from .note import Note diff -r 623836db99af -r 4556eebe5d73 light9/effect/sequencer/eval_faders.py --- a/light9/effect/sequencer/eval_faders.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,111 +0,0 @@ -import traceback -import logging -import time -from dataclasses import dataclass -from typing import List, Optional, cast - -from prometheus_client import Summary -from rdfdb import SyncedGraph -from rdflib import URIRef -from rdflib.term import Node - -from light9.effect.effect_function_library import EffectFunctionLibrary -from light9.effect.effecteval2 import EffectEval2 -from light9.effect.settings import DeviceSettings, EffectSettings -from light9.namespaces import L9, RDF -from light9.newtypes import EffectAttr, EffectUri, UnixTime -from light9.typedgraph import typedValue - -log = logging.getLogger('seq.fader') - -COMPILE = Summary('compile_graph_fader', 'compile') -COMPUTE_ALL_FADERS = Summary('compute_all_faders', 'compile') - - -@dataclass -class Fader: - graph: SyncedGraph - lib: EffectFunctionLibrary - uri: URIRef - effect: EffectUri - setEffectAttr: EffectAttr - - value: Optional[float] = None # mutable - - def __post_init__(self): - self.ee = EffectEval2(self.graph, self.effect, self.lib) - - -class FaderEval: - """peer to Sequencer, but this one takes the current :Fader settings -> sendToCollector - - """ - - def __init__(self, graph: SyncedGraph, lib: EffectFunctionLibrary): - self.graph = graph - self.lib = lib - self.faders: List[Fader] = [] - self.grandMaster = 1.0 - - self.graph.addHandler(self._compile) - self.graph.addHandler(self._compileGm) - - @COMPILE.time() - def _compile(self) -> None: - """rebuild our data from the graph""" - self.faders = [] - for fader in self.graph.subjects(RDF.type, L9['Fader']): - try: - self.faders.append(self._compileFader(fader)) - except ValueError: - pass - - # this could go in a second, smaller addHandler call to avoid rebuilding Fader objs constantly - for f in self.faders: - f.value = None - try: - setting = typedValue(Node, self.graph, f.uri, L9['setting']) - except ValueError: - continue - - try: - f.value = typedValue(float, self.graph, setting, L9['value']) - except ValueError: - continue - - def _compileFader(self, fader: URIRef) -> Fader: - effect = typedValue(EffectUri, self.graph, fader, L9['effect']) - setting = typedValue(Node, self.graph, fader, L9['setting']) - setAttr = typedValue(EffectAttr, self.graph, setting, L9['effectAttr']) - return Fader(self.graph, self.lib, cast(URIRef, fader), effect, setAttr) - - def _compileGm(self): - try: - self.grandMaster = typedValue(float, self.graph, L9.grandMaster, L9.value) - except ValueError: - return - - @COMPUTE_ALL_FADERS.time() - def computeOutput(self) -> DeviceSettings: - faderEffectOutputs: List[DeviceSettings] = [] - now = UnixTime(time.time()) - for f in self.faders: - try: - if f.value is None: - log.warning(f'{f.value=}; should be set during _compile. Skipping {f.uri}') - continue - v = f.value - v *= self.grandMaster - effectSettings = EffectSettings(self.graph, [(f.effect, f.setEffectAttr, v)]) - - ds = f.ee.compute(now, effectSettings) - faderEffectOutputs.append(ds) - except Exception: - log.warning(f'on fader {f}') - traceback.print_exc() - continue - - merged = DeviceSettings.merge(self.graph, faderEffectOutputs) - # please remove (after fixing stats display to show it) - log.debug("computed %s faders in %.1fms", len(self.faders), (time.time() - now) * 1000) - return merged diff -r 623836db99af -r 4556eebe5d73 light9/effect/sequencer/eval_faders_test.py --- a/light9/effect/sequencer/eval_faders_test.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,79 +0,0 @@ -from unittest import mock - -from light9.effect.effect_function_library import EffectFunctionLibrary -from light9.effect.sequencer.eval_faders import FaderEval -from light9.effect.settings import DeviceSettings -from light9.mock_syncedgraph import MockSyncedGraph -from light9.namespaces import L9 - -PREFIXES = ''' -@prefix : . -@prefix effect: . -@prefix rdfs: . -@prefix show: . -@prefix xsd: . -@prefix dev: . -@prefix dmxA: . -@prefix func: . -''' - -NOTE_GRAPH = PREFIXES + ''' - :brightness - a :DeviceAttr; - rdfs:label "brightness"; - :dataType :scalar . - - :strength - a :EffectAttr; - rdfs:label "strength" . - - :SimpleDimmer - a :DeviceClass; - rdfs:label "SimpleDimmer"; - :deviceAttr :brightness; - :attr [ :outputAttr :level; :dmxOffset 0 ] . - - :light1 - a :SimpleDimmer; - :dmxUniverse dmxA:; - :dmxBase 178 . - - - func:scale - a :EffectFunction; - :input - [ :effectAttr :strength; :defaultValue 0.0 ], - [ :effectAttr :deviceSettings; ] . - - - effect:effect1 - a :Effect; - :effectFunction func:scale; - :setting [:effectAttr :deviceSettings; :value [ - :setting [ - :device :light1; :deviceAttr :brightness; :value 0.5 - ] - ] ] . - - - :fade1 - a :Fader; - :effect effect:effect1; - :setting :fs1 . - :fs1 - :value 0.6 ; - :effectAttr :strength . - - ''' - - -class TestFaderEval: - - def test_faderValueScalesEffectSettings(self): - g = MockSyncedGraph(NOTE_GRAPH) - sender = mock.MagicMock() - - eff = EffectFunctionLibrary(g) - f = FaderEval(g, eff) - devSettings = f.computeOutput() - assert devSettings == DeviceSettings(g, [(L9['light1'], L9['brightness'], 0.3)]) \ No newline at end of file diff -r 623836db99af -r 4556eebe5d73 light9/effect/sequencer/note.py --- a/light9/effect/sequencer/note.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,155 +0,0 @@ -import bisect -import logging -import time -from dataclasses import dataclass -from decimal import Decimal -from typing import Any, Dict, List, Optional, Tuple, Union, cast -from light9.typedgraph import typedValue - -from rdfdb.syncedgraph.syncedgraph import SyncedGraph -from rdflib import Literal - -from light9.effect.settings import BareEffectSettings -from light9.namespaces import L9 -from light9.newtypes import (Curve, EffectAttr, NoteUri, VTUnion) - -log = logging.getLogger('sequencer') - - -def pyType(n): - ret = n.toPython() - if isinstance(ret, Decimal): - return float(ret) - return ret - - -def prettyFormat(x: Union[float, str]): - if isinstance(x, float): - return round(x, 4) - return x - - -@dataclass -class Note: - """A Note outputs EffectAttr settings. - - Sample graph: - :note1 a :Note; :curve :n1c1; :effectClass effect:allcolor; - - It can animate the EffectAttr settings over time, in two ways: - * a `timed note` has an envelope curve that animates - the :strength EffectAttr over time - * an `untimed note` has no curve, a fixed strength, but - still passes the wall clock time to its effect so the - effect can include animation. A `Fader` is an untimed note. - - This obj is immutable, I think, but the graph can change, - which can affect the output. However, I think this doesn't - do its own rebuilds, and it's up to the caller to addHandler - around the creation of Note objects. - """ - graph: SyncedGraph - uri: NoteUri - # simpleOutputs: SimpleOutputs - timed: bool = True - - def __post_init__(self): # graph ok - ec = self.graph.value(self.uri, L9['effectClass']) - if ec is None: - raise ValueError(f'note {self.uri} has no :effectClass') - self.effectClass = EffectClass(ec) - - self.baseEffectSettings = self.getBaseEffectSettings() - - if self.timed: - originTime = typedValue(float, self.graph, self.uri, L9['originTime']) - self.points: List[Tuple[float, float]] = [] - for curve in self.graph.objects(self.uri, L9['curve']): - self.points.extend(self.getCurvePoints(cast(Curve, curve), L9['strength'], originTime)) - self.points.sort() - else: - self.points = [] - self.value = typedValue(float, self.graph, self.uri, L9['value']) - - def getBaseEffectSettings(self) -> BareEffectSettings: # graph ok - """i think these are settings that are fixed over time, - e.g. that you set in the note's body in the timeline editor - """ - out: Dict[EffectAttr, VTUnion] = {} - for s in self.graph.objects(self.uri, L9['setting']): - settingValues = dict(self.graph.predicate_objects(s)) - ea = cast(EffectAttr, settingValues[L9['effectAttr']]) - out[ea] = pyType(settingValues[L9['value']]) - return BareEffectSettings(s=out) - - def getCurvePoints(self, curve: Curve, attr, originTime: float) -> List[Tuple[float, float]]: - points = [] - po = list(self.graph.predicate_objects(curve)) - if dict(po).get(L9['attr'], None) != attr: - return [] - for point in [row[1] for row in po if row[0] == L9['point']]: - po2 = dict(self.graph.predicate_objects(point)) - t = cast(Literal, po2[L9['time']]).toPython() - if not isinstance(t, float): - raise TypeError - - v = cast(Literal, po2[L9['value']]).toPython() - if not isinstance(v, float): - raise TypeError - points.append((originTime + t, v)) - return points - - def activeAt(self, t: float) -> bool: - return self.points[0][0] <= t <= self.points[-1][0] - - def evalCurve(self, t: float) -> float: - i = bisect.bisect_left(self.points, (t, None)) - 1 - - if i == -1: - return self.points[0][1] - if self.points[i][0] > t: - return self.points[i][1] - if i >= len(self.points) - 1: - return self.points[i][1] - - p1, p2 = self.points[i], self.points[i + 1] - frac = (t - p1[0]) / (p2[0] - p1[0]) - y = p1[1] + (p2[1] - p1[1]) * frac - return y - - def outputCurrent(self): # no graph - - return self._outputSettings(t=None, strength=self.value) - - def _outputSettings( - self, - t: float | None, - strength: Optional[float] = None # - ) -> Tuple[BareEffectSettings, Dict]: # no graph - - if t is None: - if self.timed: - raise TypeError() - t = time.time() # so live effects will move - report: Dict[str, Any] = { - 'note': str(self.uri), - 'effectClass': str(self.effectClass), - } - - s = self.evalCurve(t) if strength is None else strength - out = self.baseEffectSettings.withStrength(s) - report['effectSettings'] = dict((str(k), prettyFormat(v)) for k, v in sorted(out.s.items())) - report['nonZero'] = s > 0 - - return out, report - - # old api had this - - startTime = self.points[0][0] if self.timed else 0 - out, evalReport = self.effectEval.outputFromEffect( - effectSettings, - songTime=t, - # note: not using origin here since it's going away - noteTime=t - startTime) - report['devicesAffected'] = len(out.devices()) - return out, report diff -r 623836db99af -r 4556eebe5d73 light9/effect/sequencer/note_test.py --- a/light9/effect/sequencer/note_test.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,78 +0,0 @@ -import pytest - -from light9.effect.sequencer import Note -from light9.effect.settings import BareEffectSettings -from light9.mock_syncedgraph import MockSyncedGraph -from light9.namespaces import L9 -from light9.newtypes import EffectAttr, NoteUri - -PREFIXES = ''' -@prefix : . -@prefix effect: . -@prefix rdfs: . -@prefix show: . -@prefix xsd: . -@prefix dev: . -@prefix dmxA: . -''' - -FADER_GRAPH = PREFIXES + ''' - :fade1 - a :Fader; - :effectClass effect:effect1; - :effectAttr :strength; - :value 0.6 . -''' - - -# class TestUntimedFaderNote: - -# def test_returnsEffectSettings(self): -# g = MockSyncedGraph(FADER_GRAPH) -# n = Note(g, NoteUri(L9['fade1']), timed=False) -# out, report = n.outputCurrent() -# assert report['effectSettings'] == {'http://light9.bigasterisk.com/strength': 0.6} -# assert out == BareEffectSettings(s={EffectAttr(L9['strength']): 0.6}) - - -NOTE_GRAPH = PREFIXES + ''' - :brightness - a :DeviceAttr; - rdfs:label "brightness"; - :dataType :scalar . - - :strength - a :EffectAttr; - rdfs:label "strength" . - - :SimpleDimmer - a :DeviceClass; - rdfs:label "SimpleDimmer"; - :deviceAttr :brightness; - :attr [ :outputAttr :level; :dmxOffset 0 ] . - - dev:light1 - a :SimpleDimmer; - :dmxUniverse dmxA:; - :dmxBase 178 . - - effect:effect1 - a :EffectClass; - :setting effect:effect1_set1 . - effect:effect1_set1 - :device dev:light1; - :deviceAttr :brightness; - :scaledValue 0.5 . - :fade1 - a :Fader; - :effectClass effect:effect1; - :effectAttr :strength; - :value 0.6 . - ''' - - -class TestTimedNote: - - @pytest.mark.skip() - def test_scalesStrengthWithCurve(self): - pass diff -r 623836db99af -r 4556eebe5d73 light9/effect/sequencer/sequencer.py --- a/light9/effect/sequencer/sequencer.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,168 +0,0 @@ -''' -copies from effectloop.py, which this should replace -''' - -import asyncio -import importlib -import logging -import time -import traceback -from typing import Callable, Coroutine, Dict, List, cast - -from louie import All, dispatcher -from rdfdb.syncedgraph.syncedgraph import SyncedGraph -from rdflib import URIRef -from twisted.internet import reactor -from twisted.internet.inotify import INotify -from twisted.python.filepath import FilePath - -from light9.ascoltami.musictime_client import MusicTime -from light9.effect import effecteval -from light9.effect.sequencer import Note -from light9.effect.settings import DeviceSettings -from light9.metrics import metrics -from light9.namespaces import L9, RDF -from light9.newtypes import NoteUri, Song - -log = logging.getLogger('sequencer') - - -class StateUpdate(All): - pass - - -class CodeWatcher: - - def __init__(self, onChange): - self.onChange = onChange - - self.notifier = INotify() - self.notifier.startReading() - self.notifier.watch(FilePath(effecteval.__file__.replace('.pyc', - '.py')), - callbacks=[self.codeChange]) - - def codeChange(self, watch, path, mask): - - def go(): - log.info("reload effecteval") - importlib.reload(effecteval) - self.onChange() - - # in case we got an event at the start of the write - reactor.callLater(.1, go) # type: ignore - - -class Sequencer: - """Notes from the graph + current song playback -> sendToCollector""" - def __init__(self, - graph: SyncedGraph, - sendToCollector: Callable[[DeviceSettings], Coroutine[None ,None,None]], - fps=40, - ): - self.graph = graph - self.fps = fps - metrics('update_loop_goal_fps').set(self.fps) - metrics('update_loop_goal_latency').set(1 / self.fps) - self.sendToCollector = sendToCollector - self.music = MusicTime(period=.2) - - self.recentUpdateTimes: List[float] = [] - self.lastStatLog = 0.0 - self._compileGraphCall = None - self.notes: Dict[Song, List[Note]] = {} # song: [notes] - self.simpleOutputs = SimpleOutputs(self.graph) - self.graph.addHandler(self.compileGraph) - self.lastLoopSucceeded = False - - # self.codeWatcher = CodeWatcher(onChange=self.onCodeChange) - asyncio.create_task(self.updateLoop()) - - def onCodeChange(self): - log.debug('seq.onCodeChange') - self.graph.addHandler(self.compileGraph) - #self.updateLoop() - - def compileGraph(self) -> None: - """rebuild our data from the graph""" - for song in self.graph.subjects(RDF.type, L9['Song']): - - def compileSong(song: Song = cast(Song, song)) -> None: - self.compileSong(song) - - self.graph.addHandler(compileSong) - - def compileSong(self, song: Song) -> None: - anyErrors = False - self.notes[song] = [] - for note in self.graph.objects(song, L9['note']): - try: - n = Note(self.graph, NoteUri(cast(NoteUri, note))) - except Exception: - log.warn(f"failed to build Note {note} - skipping") - anyErrors = True - continue - self.notes[song].append(n) - if not anyErrors: - log.info(f'built all notes for {song}') - - async def updateLoop(self): - while True: - frameStart = time.time() - try: - sec = await self.update() - except Exception as e: - self.lastLoopSucceeded = False - traceback.print_exc() - log.warn('updateLoop: %r', e) - await asyncio.sleep(1) - continue - else: - took = time.time() - frameStart - metrics('update_loop_latency').observe(took) - - if not self.lastLoopSucceeded: - log.info('Sequencer.update is working') - self.lastLoopSucceeded = True - - delay = max(0, 1 / self.fps - took) - await asyncio.sleep(delay) - continue - - async def update(self): - with metrics('update_s0_getMusic').time(): - musicState = {'t':123.0,'song':'http://light9.bigasterisk.com/show/dance2019/song5'}#self.music.getLatest() - if not musicState.get('song') or not isinstance( - musicState.get('t'), float): - return - song = Song(URIRef(musicState['song'])) - # print('dispsend') - # import pdb;pdb.set_trace() - dispatcher.send(StateUpdate, - update={ - 'song': str(song), - 't': musicState['t'] - }) - - with metrics('update_s1_eval').time(): - settings = [] - songNotes = sorted(cast(List[Note], self.notes.get(song, [])), key=lambda n: n.uri) - noteReports = [] - for note in songNotes: - try: - s, report = note.outputSettings(musicState['t']) - except Exception: - traceback.print_exc() - raise - noteReports.append(report) - settings.append(s) - devSettings = DeviceSettings.fromList(self.graph, settings) - dispatcher.send(StateUpdate, update={'songNotes': noteReports}) - - with metrics('update_s3_send').time(): # our measurement - sendSecs = await self.sendToCollector(devSettings) - - # sendToCollector's own measurement. - # (sometimes it's None, not sure why, and neither is mypy) - #if isinstance(sendSecs, float): - # metrics('update_s3_send_client').observe(sendSecs) diff -r 623836db99af -r 4556eebe5d73 light9/effect/sequencer/service.py --- a/light9/effect/sequencer/service.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,85 +0,0 @@ -""" -plays back effect notes from the timeline (and an untimed note from the faders) -""" - -import asyncio -import json -import logging -import time - -from louie import dispatcher -from rdfdb.syncedgraph.syncedgraph import SyncedGraph -from sse_starlette.sse import EventSourceResponse -from starlette.applications import Starlette -from starlette.routing import Route -from starlette_exporter import PrometheusMiddleware, handle_metrics - -from lib.background_loop import loop_forever -from light9 import networking -from light9.collector.collector_client_asyncio import sendToCollector -from light9.effect.effect_function_library import EffectFunctionLibrary -from light9.effect.sequencer.eval_faders import FaderEval -from light9.effect.sequencer.sequencer import Sequencer, StateUpdate -from light9.run_local import log - -RATE = 20 - - -async def changes(): - state = {} - q = asyncio.Queue() - - def onBroadcast(update): - state.update(update) - q.put_nowait(None) - - dispatcher.connect(onBroadcast, StateUpdate) - - lastSend = 0 - while True: - await q.get() - now = time.time() - if now > lastSend + .2: - lastSend = now - yield json.dumps(state) - - -async def send_page_updates(request): - return EventSourceResponse(changes()) - - -def main(): - graph = SyncedGraph(networking.rdfdb.url, "effectSequencer") - logging.getLogger('sse_starlette.sse').setLevel(logging.INFO) - - logging.getLogger('autodepgraphapi').setLevel(logging.INFO) - logging.getLogger('syncedgraph').setLevel(logging.INFO) - - logging.getLogger('effecteval').setLevel(logging.INFO) - logging.getLogger('seq.fader').setLevel(logging.INFO) - - # seq = Sequencer(graph, send) # per-song timed notes - lib = EffectFunctionLibrary(graph) - faders = FaderEval(graph, lib) # bin/fade's untimed effects - - #@metrics('computeAndSend').time() # needs rework with async - async def update(first_run): - ds = faders.computeOutput() - await sendToCollector('effectSequencer', session='0', settings=ds) - - faders_loop = loop_forever(func=update, metric_prefix='faders', sleep_period=1 / RATE) - - app = Starlette( - debug=True, - routes=[ - Route('/updates', endpoint=send_page_updates), - ], - ) - - app.add_middleware(PrometheusMiddleware) - app.add_route("/metrics", handle_metrics) - - return app - - -app = main() diff -r 623836db99af -r 4556eebe5d73 light9/effect/sequencer/service_test.py --- a/light9/effect/sequencer/service_test.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,13 +0,0 @@ -import asyncio - -from light9.run_local import log - - -def test_import(): - - async def go(): - # this sets up some watcher tasks - from light9.effect.sequencer.service import app - print(app) - - asyncio.run(go(), debug=True) \ No newline at end of file diff -r 623836db99af -r 4556eebe5d73 light9/effect/sequencer/web/Light9SequencerUi.ts --- a/light9/effect/sequencer/web/Light9SequencerUi.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,166 +0,0 @@ -import debug from "debug"; -import { css, html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { NamedNode } from "n3"; -import { getTopGraph } from "../../../web/RdfdbSyncedGraph"; -import { SyncedGraph } from "../../../web/SyncedGraph"; - -debug.enable("*"); -const log = debug("sequencer"); - -interface Note { - note: string; - nonZero: boolean; - rowClass?: string; // added in message handler below - effectClass: string; - effectSettings: { [attr: string]: string }; - effectSettingsPairs: EffectSettingsPair[]; // added in message handler below - devicesAffected: number; -} -interface Report { - song: string; - songUri: NamedNode; // added in message handler below - t: number; - roundT?: number; // added in message handler below - recentFps: number; - recentDeltas: number[]; - recentDeltasStyle: string[]; // added in message handler below - songNotes: Note[]; -} -interface EffectSettingsPair { - effectAttr: string; - value: any; -} -@customElement("light9-sequencer-ui") -export class Light9SequencerUi extends LitElement { - static styles = [ - css` - :host { - display: block; - } - td { - white-space: nowrap; - padding: 0 10px; - vertical-align: top; - vertical-align: top; - text-align: start; - } - tr.active { - background: #151515; - } - .inactive > * { - opacity: 0.5; - } - .effectSetting { - display: inline-block; - background: #1b1e21; - margin: 1px 3px; - } - .chart { - height: 40px; - background: #222; - display: inline-flex; - align-items: flex-end; - } - .chart > div { - background: #a4a54f; - width: 8px; - margin: 0 1px; - } - .number { - display: inline-block; - min-width: 4em; - } - `, - ]; - render() { - return [ - html` - -

Sequencer [metrics]

- -

Song

`, - this.report - ? html` - - - t=${this.report.roundT} - -

Notes

- - - - - - - - - ${this.report.songNotes.map( - (item: Note) => html` - - - - - - - ` - )} -
NoteEffect classEffect settingsDevices affected
- ${item.effectSettingsPairs.map( - (item) => html` -
- - : - ${item.value} - -
- ` - )} -
${item.devicesAffected}
- ` - : html`waiting for first report...`, - ]; - } - - graph!: SyncedGraph; - @property() report!: Report; - - constructor() { - super(); - getTopGraph().then((g) => { - this.graph = g; - const source = new EventSource("./api/updates"); - source.addEventListener("message", this.onMessage.bind(this)); - }); - } - onMessage(ev: MessageEvent) { - const report = JSON.parse(ev.data) as Report; - report.roundT = Math.floor((report.t || 0) * 1000) / 1000; - report.recentFps = Math.floor((report.recentFps || 0) * 10) / 10; - report.recentDeltasStyle = (report.recentDeltas || []).map((dt) => { - const height = Math.min(40, (dt / 0.085) * 20); - return `height: ${height}px;`; - }); - report.songUri = this.graph.Uri(report.song); - - const fakeUris = (report.songNotes || []).map((obj) => { - return { value: obj.note, orig: obj }; - }); - const s = this.graph.sortedUris(fakeUris); - report.songNotes = s.map((u) => { - return u.orig; - }); - - (report.songNotes || []).forEach((note) => { - note.rowClass = note.nonZero ? "active" : "inactive"; - note.effectSettingsPairs = []; - - const attrs = Object.keys(note.effectSettings); - attrs.sort(); - attrs.forEach((attr) => { - note.effectSettingsPairs.push({ effectAttr: attr, value: note.effectSettings[attr] } as EffectSettingsPair); - }); - }); - this.report = report; - } -} diff -r 623836db99af -r 4556eebe5d73 light9/effect/sequencer/web/index.html --- a/light9/effect/sequencer/web/index.html Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,13 +0,0 @@ - - - - effect sequencer - - - - - - - - - diff -r 623836db99af -r 4556eebe5d73 light9/effect/sequencer/web/vite.config.ts --- a/light9/effect/sequencer/web/vite.config.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ -import { defineConfig } from "vite"; - -const servicePort = 8213; -export default defineConfig({ - base: "/effectSequencer/", - root: "./light9/effect/sequencer/web", - publicDir: "../web", - server: { - host: "0.0.0.0", - strictPort: true, - port: servicePort + 100, - hmr: { - port: servicePort + 200, - }, - }, - clearScreen: false, - define: { - global: {}, - }, -}); diff -r 623836db99af -r 4556eebe5d73 light9/effect/settings.py --- a/light9/effect/settings.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,311 +0,0 @@ -""" -Data structure and convertors for a table of (device,attr,value) -rows. These might be effect attrs ('strength'), device attrs ('rx'), -or output attrs (dmx channel). - -BareSettings means (attr,value), no device. -""" -from __future__ import annotations - -import decimal -import logging -from dataclasses import dataclass -from typing import Any, Dict, Iterable, List, Sequence, Set, Tuple, cast - -import numpy -from rdfdb.syncedgraph.syncedgraph import SyncedGraph -from rdflib import Literal, URIRef - -from light9.collector.device import resolve -from light9.localsyncedgraph import LocalSyncedGraph -from light9.namespaces import L9, RDF -from light9.newtypes import (DeviceAttr, DeviceUri, EffectAttr, HexColor, VTUnion) - -log = logging.getLogger('settings') - - -def parseHex(h): - if h[0] != '#': - raise ValueError(h) - return [int(h[i:i + 2], 16) for i in (1, 3, 5)] - - -def parseHexNorm(h): - return [x / 255 for x in parseHex(h)] - - -def toHex(rgbFloat: Sequence[float]) -> HexColor: - assert len(rgbFloat) == 3 - scaled = (max(0, min(255, int(v * 255))) for v in rgbFloat) - return HexColor('#%02x%02x%02x' % tuple(scaled)) - - -def getVal(graph, subj): - lit = graph.value(subj, L9['value']) or graph.value(subj, L9['scaledValue']) - ret = lit.toPython() - if isinstance(ret, decimal.Decimal): - ret = float(ret) - return ret - - -GraphType = SyncedGraph | LocalSyncedGraph - - -class _Settings: - """ - Generic for DeviceUri/DeviceAttr/VTUnion or EffectClass/EffectAttr/VTUnion - - default values are 0 or '#000000'. Internal rep must not store zeros or some - comparisons will break. - """ - EntityType = DeviceUri - AttrType = DeviceAttr - - def __init__(self, graph: GraphType, settingsList: List[Tuple[Any, Any, VTUnion]]): - self.graph = graph # for looking up all possible attrs - self._compiled: Dict[self.__class__.EntityType, Dict[self.__class__.AttrType, VTUnion]] = {} - for e, a, v in settingsList: - attrVals = self._compiled.setdefault(e, {}) - if a in attrVals: - v = resolve( - e, # Hey, this is supposed to be DeviceClass (which is not convenient for us), but so far resolve() doesn't use that arg - a, - [attrVals[a], v]) - attrVals[a] = v - # self._compiled may not be final yet- see _fromCompiled - self._delZeros() - - @classmethod - def _fromCompiled(cls, graph: GraphType, compiled: Dict[EntityType, Dict[AttrType, VTUnion]]): - obj = cls(graph, []) - obj._compiled = compiled - obj._delZeros() - return obj - - @classmethod - def fromList(cls, graph: GraphType, others: List[_Settings]): - """note that others may have multiple values for an attr""" - self = cls(graph, []) - for s in others: - # if not isinstance(s, cls): - # raise TypeError(s) - for row in s.asList(): # could work straight from s._compiled - if row[0] is None: - raise TypeError('bad row %r' % (row,)) - dev, devAttr, value = row - devDict = self._compiled.setdefault(dev, {}) - if devAttr in devDict: - existingVal: VTUnion = devDict[devAttr] - # raise NotImplementedError('fixme: dev is to be a deviceclass (but it is currently unused)') - value = resolve(dev, devAttr, [existingVal, value]) - devDict[devAttr] = value - self._delZeros() - return self - - @classmethod - def _mult(cls, weight, row, dd) -> VTUnion: - if isinstance(row[2], str): - prev = parseHexNorm(dd.get(row[1], '#000000')) - return toHex(prev + weight * numpy.array(parseHexNorm(row[2]))) - else: - return dd.get(row[1], 0) + weight * row[2] - - @classmethod - def fromBlend(cls, graph: GraphType, others: List[Tuple[float, _Settings]]): - """others is a list of (weight, Settings) pairs""" - out = cls(graph, []) - for weight, s in others: - if not isinstance(s, cls): - raise TypeError(s) - for row in s.asList(): # could work straight from s._compiled - if row[0] is None: - raise TypeError('bad row %r' % (row,)) - dd = out._compiled.setdefault(row[0], {}) - - newVal = cls._mult(weight, row, dd) - dd[row[1]] = newVal - out._delZeros() - return out - - def _zeroForAttr(self, attr: AttrType) -> VTUnion: - if attr == L9['color']: - return HexColor('#000000') - return 0.0 - - def _delZeros(self): - for dev, av in list(self._compiled.items()): - for attr, val in list(av.items()): - if val == self._zeroForAttr(attr): - del av[attr] - if not av: - del self._compiled[dev] - - def __hash__(self): - itemed = tuple([(d, tuple([(a, v) for a, v in sorted(av.items())])) for d, av in sorted(self._compiled.items())]) - return hash(itemed) - - def __eq__(self, other): - if not issubclass(other.__class__, self.__class__): - raise TypeError("can't compare %r to %r" % (self.__class__, other.__class__)) - return self._compiled == other._compiled - - def __ne__(self, other): - return not self == other - - def __bool__(self): - return bool(self._compiled) - - def __repr__(self): - words = [] - - def accum(): - for dev, av in self._compiled.items(): - for attr, val in sorted(av.items()): - words.append('%s.%s=%s' % (dev.rsplit('/')[-1], attr.rsplit('/')[-1], val)) - if len(words) > 5: - words.append('...') - return - - accum() - if not words: - words = ['(no settings)'] - return '<%s %s>' % (self.__class__.__name__, ' '.join(words)) - - def getValue(self, dev: EntityType, attr: AttrType, defaultToZero=True): - x = self._compiled.get(dev, {}) - if defaultToZero: - return x.get(attr, self._zeroForAttr(attr)) - else: - return x[attr] - - def _vectorKeys(self, deviceAttrFilter=None): - """stable order of all the dev,attr pairs for this type of settings""" - raise NotImplementedError - - def asList(self) -> List[Tuple[EntityType, AttrType, VTUnion]]: - """old style list of (dev, attr, val) tuples""" - out = [] - for dev, av in self._compiled.items(): - for attr, val in av.items(): - out.append((dev, attr, val)) - return out - - def devices(self) -> List[EntityType]: - return list(self._compiled.keys()) - - def toVector(self, deviceAttrFilter=None) -> List[float]: - out: List[float] = [] - for dev, attr in self._vectorKeys(deviceAttrFilter): - v = self.getValue(dev, attr) - if attr == L9['color']: - out.extend(parseHexNorm(v)) - else: - if not isinstance(v, float): - raise TypeError(f'{attr=} value was {v=}') - out.append(v) - return out - - def byDevice(self) -> Iterable[Tuple[EntityType, _Settings]]: - for dev, av in self._compiled.items(): - yield dev, self.__class__._fromCompiled(self.graph, {dev: av}) - - def ofDevice(self, dev: EntityType) -> _Settings: - return self.__class__._fromCompiled(self.graph, {dev: self._compiled.get(dev, {})}) - - def distanceTo(self, other): - diff = numpy.array(self.toVector()) - other.toVector() - d = numpy.linalg.norm(diff, ord=None) - log.info('distanceTo %r - %r = %g', self, other, d) - return d - - def statements(self, subj: EntityType, ctx: URIRef, settingRoot: URIRef, settingsSubgraphCache: Set): - """ - settingRoot can be shared across images (or even wider if you want) - """ - # ported from live.coffee - add = [] - for i, (dev, attr, val) in enumerate(self.asList()): - # hopefully a unique number for the setting so repeated settings converge - settingHash = hash((dev, attr, val)) % 9999999 - setting = URIRef('%sset%s' % (settingRoot, settingHash)) - add.append((subj, L9['setting'], setting, ctx)) - if setting in settingsSubgraphCache: - continue - - scaledAttributeTypes = [L9['color'], L9['brightness'], L9['uv']] - settingType = L9['scaledValue'] if attr in scaledAttributeTypes else L9['value'] - if not isinstance(val, URIRef): - val = Literal(val) - add.extend([ - (setting, L9['device'], dev, ctx), - (setting, L9['deviceAttr'], attr, ctx), - (setting, settingType, val, ctx), - ]) - settingsSubgraphCache.add(setting) - - return add - - -class DeviceSettings(_Settings): - EntityType = DeviceUri - AttrType = DeviceAttr - - def _vectorKeys(self, deviceAttrFilter=None): - with self.graph.currentState() as g: - devs = set() # devclass, dev - for dc in g.subjects(RDF.type, L9['DeviceClass']): - for dev in g.subjects(RDF.type, dc): - devs.add((dc, dev)) - - keys = [] - for dc, dev in sorted(devs): - for attr in sorted(g.objects(dc, L9['deviceAttr'])): - key = (dev, attr) - if deviceAttrFilter and key not in deviceAttrFilter: - continue - keys.append(key) - return keys - - @classmethod - def fromResource(cls, graph: GraphType, subj: EntityType): - settingsList = [] - with graph.currentState() as g: - for s in g.objects(subj, L9['setting']): - d = g.value(s, L9['device']) - da = g.value(s, L9['deviceAttr']) - v = getVal(g, s) - settingsList.append((d, da, v)) - return cls(graph, settingsList) - - @classmethod - def fromVector(cls, graph, vector, deviceAttrFilter=None): - compiled: Dict[DeviceSettings.EntityType, Dict[DeviceSettings.AttrType, VTUnion]] = {} - i = 0 - for (d, a) in cls(graph, [])._vectorKeys(deviceAttrFilter): - if a == L9['color']: - v = toHex(vector[i:i + 3]) - i += 3 - else: - v = vector[i] - i += 1 - compiled.setdefault(d, {})[a] = v - return cls._fromCompiled(graph, compiled) - - @classmethod - def merge(cls, graph: SyncedGraph, others: List[DeviceSettings]) -> DeviceSettings: - return cls.fromList(graph, cast(List[_Settings], others)) - - -@dataclass -class BareEffectSettings: - # settings for an already-selected EffectClass - s: Dict[EffectAttr, VTUnion] - - def withStrength(self, strength: float) -> BareEffectSettings: - out = self.s.copy() - out[EffectAttr(L9['strength'])] = strength - return BareEffectSettings(s=out) - - -class EffectSettings(_Settings): - pass diff -r 623836db99af -r 4556eebe5d73 light9/effect/settings_test.py --- a/light9/effect/settings_test.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,151 +0,0 @@ -import unittest -from typing import cast - -from rdfdb.patch import Patch -from rdflib import Literal - -from light9.effect.settings import DeviceSettings -from light9.localsyncedgraph import LocalSyncedGraph -from light9.namespaces import DEV, L9 -from light9.newtypes import DeviceAttr, DeviceUri, HexColor, VTUnion, decimalLiteral - - -class TestDeviceSettings(unittest.TestCase): - - def setUp(self): - self.graph = LocalSyncedGraph(files=['test/cam/lightConfig.n3', 'test/cam/bg.n3']) - - def testToVectorZero(self): - ds = DeviceSettings(self.graph, []) - self.assertEqual([0] * 30, ds.toVector()) - - def testEq(self): - s1 = DeviceSettings(self.graph, [ - (L9['light1'], L9['attr1'], 0.5), - (L9['light1'], L9['attr2'], 0.3), - ]) - s2 = DeviceSettings(self.graph, [ - (L9['light1'], L9['attr2'], 0.3), - (L9['light1'], L9['attr1'], 0.5), - ]) - self.assertTrue(s1 == s2) - self.assertFalse(s1 != s2) - - def testMissingFieldsEqZero(self): - self.assertEqual(DeviceSettings(self.graph, [ - (L9['aura1'], L9['rx'], 0), - ]), DeviceSettings(self.graph, [])) - - def testFalseIfZero(self): - self.assertTrue(DeviceSettings(self.graph, [(L9['aura1'], L9['rx'], 0.1)])) - self.assertFalse(DeviceSettings(self.graph, [])) - - def testFromResource(self): - ctx = L9[''] - self.graph.patch( - Patch(addQuads=[ - (L9['foo'], L9['setting'], L9['foo_set0'], ctx), - (L9['foo_set0'], L9['device'], L9['light1'], ctx), - (L9['foo_set0'], L9['deviceAttr'], L9['brightness'], ctx), - (L9['foo_set0'], L9['value'], decimalLiteral(0.1), ctx), - (L9['foo'], L9['setting'], L9['foo_set1'], ctx), - (L9['foo_set1'], L9['device'], L9['light1'], ctx), - (L9['foo_set1'], L9['deviceAttr'], L9['speed'], ctx), - (L9['foo_set1'], L9['scaledValue'], decimalLiteral(0.2), ctx), - ])) - s = DeviceSettings.fromResource(self.graph, DeviceUri(L9['foo'])) - - self.assertEqual(DeviceSettings(self.graph, [ - (L9['light1'], L9['brightness'], 0.1), - (L9['light1'], L9['speed'], 0.2), - ]), s) - - def testToVector(self): - v = DeviceSettings(self.graph, [ - (DeviceUri(DEV['aura1']), DeviceAttr(L9['rx']), 0.5), - (DeviceUri(DEV['aura1']), DeviceAttr(L9['color']), HexColor('#00ff00')), - ]).toVector() - self.assertEqual([0, 1, 0, 0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], v) - - def testFromVector(self): - s = DeviceSettings.fromVector(self.graph, [0, 1, 0, 0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - - self.assertEqual( - DeviceSettings(self.graph, [ - (DeviceUri(DEV['aura1']), DeviceAttr(L9['rx']), 0.5), - (DeviceUri(DEV['aura1']), DeviceAttr(L9['color']), HexColor('#00ff00')), - ]), s) - - def testAsList(self): - sets = [ - (DeviceUri(L9['light1']), DeviceAttr(L9['attr2']), cast(VTUnion, 0.3)), - (DeviceUri(L9['light1']), DeviceAttr(L9['attr1']), 0.5), - ] - self.assertCountEqual(sets, DeviceSettings(self.graph, sets).asList()) - - def testDevices(self): - s = DeviceSettings(self.graph, [ - (DEV['aura1'], L9['rx'], 0), - (DEV['aura2'], L9['rx'], 0.1), - ]) - # aura1 is all defaults (zeros), so it doesn't get listed - self.assertCountEqual([DEV['aura2']], s.devices()) - - def testAddStatements(self): - s = DeviceSettings(self.graph, [ - (DEV['aura2'], L9['rx'], 0.1), - ]) - stmts = s.statements(DeviceUri(L9['foo']), L9['ctx1'], L9['s_'], set()) - self.maxDiff = None - setting = sorted(stmts)[-1][0] - self.assertCountEqual([ - (L9['foo'], L9['setting'], setting, L9['ctx1']), - (setting, L9['device'], DEV['aura2'], L9['ctx1']), - (setting, L9['deviceAttr'], L9['rx'], L9['ctx1']), - (setting, L9['value'], Literal(0.1), L9['ctx1']), - ], stmts) - - def testDistanceTo(self): - s1 = DeviceSettings(self.graph, [ - (DEV['aura1'], L9['rx'], 0.1), - (DEV['aura1'], L9['ry'], 0.6), - ]) - s2 = DeviceSettings(self.graph, [ - (DEV['aura1'], L9['rx'], 0.3), - (DEV['aura1'], L9['ry'], 0.3), - ]) - self.assertEqual(0.36055512754639896, s1.distanceTo(s2)) - - -L1 = L9['light1'] -ZOOM = L9['zoom'] - - -class TestFromBlend(unittest.TestCase): - - def setUp(self): - self.graph = LocalSyncedGraph(files=['test/cam/lightConfig.n3', 'test/cam/bg.n3']) - - def testSingle(self): - self.assertEqual(DeviceSettings(self.graph, [(L1, ZOOM, 0.5)]), - DeviceSettings.fromBlend(self.graph, [(1, DeviceSettings(self.graph, [(L1, ZOOM, 0.5)]))])) - - def testScale(self): - self.assertEqual(DeviceSettings(self.graph, [(L1, ZOOM, 0.1)]), - DeviceSettings.fromBlend(self.graph, [(.2, DeviceSettings(self.graph, [(L1, ZOOM, 0.5)]))])) - - def testMixFloats(self): - self.assertEqual( - DeviceSettings(self.graph, [(L1, ZOOM, 0.4)]), - DeviceSettings.fromBlend(self.graph, [ - (.2, DeviceSettings(self.graph, [(L1, ZOOM, 0.5)])), - (.3, DeviceSettings(self.graph, [(L1, ZOOM, 1.0)])), - ])) - - def testMixColors(self): - self.assertEqual( - DeviceSettings(self.graph, [(L1, ZOOM, HexColor('#503000'))]), - DeviceSettings.fromBlend(self.graph, [ - (.25, DeviceSettings(self.graph, [(L1, ZOOM, HexColor('#800000'))])), - (.5, DeviceSettings(self.graph, [(L1, ZOOM, HexColor('#606000'))])), - ])) diff -r 623836db99af -r 4556eebe5d73 light9/effecteval/__init__.py --- a/light9/effecteval/__init__.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ - diff -r 623836db99af -r 4556eebe5d73 light9/effecteval/effect-components.html --- a/light9/effecteval/effect-components.html Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,109 +0,0 @@ - - - - - - - - - - - - - - - - - - diff -r 623836db99af -r 4556eebe5d73 light9/effecteval/effect.coffee --- a/light9/effecteval/effect.coffee Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ -qs = new QueryString() -model = - toSave: - uri: ko.observable(qs.value('uri')) - codeLines: ko.observableArray([]) - -socket = reconnectingWebSocket "../effectUpdates" + window.location.search, (msg) -> - console.log('effectData ' + JSON.stringify(msg)) - model.toSave.codeLines(msg.codeLines.map((x) -> {text: ko.observable(x)})) if msg.codeLines? - -model.saveCode = -> - $.ajax - type: 'PUT' - url: 'code' - data: ko.toJS(model.toSave) - -writeBack = ko.computed(model.saveCode) - -ko.applyBindings(model) - \ No newline at end of file diff -r 623836db99af -r 4556eebe5d73 light9/effecteval/effect.html --- a/light9/effecteval/effect.html Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,27 +0,0 @@ - - - - effect - - - - - -
starting...
- - Effects / - -
-
- code: - -
-
- - - - - - - - diff -r 623836db99af -r 4556eebe5d73 light9/effecteval/effect.py --- a/light9/effecteval/effect.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,192 +0,0 @@ -import re, logging -import toposort -from rdflib import URIRef -from light9.namespaces import L9, RDF -from light9.curvecalc.curve import CurveResource -from light9 import prof -from light9 import Submaster -from light9 import Effects # gets reload() later -log = logging.getLogger('effect') - -# consider http://waxeye.org/ for a parser that can be used in py and js - - -class CouldNotConvert(TypeError): - pass - - -class CodeLine: - """code string is immutable""" - - def __init__(self, graph, code): - self.graph, self.code = graph, code - - self.outName, self.inExpr, self.expr, self.resources = self._asPython() - self.pyResources = self._resourcesAsPython(self.resources) - self.possibleVars = self.findVars(self.inExpr) - - @prof.logTime - def _asPython(self): - """ - out = sub(, intensity=) - becomes - 'out', - 'sub(_u1, intensity=curve(_u2, t))', - {'_u1': URIRef('uri1'), '_u2': URIRef('uri2')} - """ - lname, expr = [s.strip() for s in self.code.split('=', 1)] - self.uriCounter = 0 - resources = {} - - def alreadyInFunc(prefix, s, i): - return i >= len(prefix) and s[i - len(prefix):i] == prefix - - def repl(m): - v = '_res%s' % self.uriCounter - self.uriCounter += 1 - r = resources[v] = URIRef(m.group(1)) - for uriTypeMatches, wrapFuncName, addlArgs in [ - (self._uriIsCurve(r), 'curve', ', t'), - # I'm pretty sure this shouldn't be auto-applied: it's reasonable to refer to a sub and not want its current value - #(self._uriIsSub(r), 'currentSubLevel', ''), - ]: - if uriTypeMatches: - if not alreadyInFunc(wrapFuncName + '(', m.string, - m.start()): - return '%s(%s%s)' % (wrapFuncName, v, addlArgs) - return v - - outExpr = re.sub(r'<(http\S*?)>', repl, expr) - return lname, expr, outExpr, resources - - def findVars(self, expr): - """may return some more strings than just the vars""" - withoutUris = re.sub(r'<(http\S*?)>', 'None', expr) - tokens = set(re.findall(r'\b([a-zA-Z_]\w*)\b', withoutUris)) - tokens.discard('None') - return tokens - - def _uriIsCurve(self, uri): - # this result could vary with graph changes (rare) - return self.graph.contains((uri, RDF.type, L9['Curve'])) - - def _uriIsSub(self, uri): - return self.graph.contains((uri, RDF.type, L9['Submaster'])) - - @prof.logTime - def _resourcesAsPython(self, resources): - """ - mapping of the local names for uris in the code to high-level - objects (Submaster, Curve) - """ - out = {} - subs = prof.logTime(Submaster.get_global_submasters)(self.graph) - for localVar, uri in list(resources.items()): - - for rdfClass in self.graph.objects(uri, RDF.type): - if rdfClass == L9['Curve']: - cr = CurveResource(self.graph, uri) - # this is slow- pool these curves somewhere, maybe just with curveset - prof.logTime(cr.loadCurve)() - out[localVar] = cr.curve - break - elif rdfClass == L9['Submaster']: - out[localVar] = subs.get_sub_by_uri(uri) - break - else: - out[localVar] = CouldNotConvert(uri) - break - else: - out[localVar] = CouldNotConvert(uri) - - return out - - -class EffectNode: - - 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) - - @prof.logTime - 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.sortCodes() - - #reload(Effects) - self.otherFuncs = prof.logTime(Effects.configExprGlobals)() - - def sortCodes(self): - """put self.codes in a working evaluation order""" - codeFromOutput = dict((c.outName, c) for c in self.codes) - deps = {} - for c in self.codes: - outName = c.outName - inNames = c.possibleVars.intersection(list(codeFromOutput.keys())) - inNames.discard(outName) - deps[outName] = inNames - self.codes = [ - codeFromOutput[n] for n in toposort.toposort_flatten(deps) - ] - - def _currentSubSettingValues(self, sub): - """what KC subSettings are setting levels right now?""" - cs = self.graph.currentState - with cs(tripleFilter=(None, L9['sub'], sub)) as g1: - for subj in g1.subjects(L9['sub'], sub): - with cs(tripleFilter=(subj, None, None)) as g2: - if (subj, RDF.type, L9['SubSetting']) in g2: - v = g2.value(subj, L9['level']).toPython() - yield v - - def currentSubLevel(self, uri): - """what's the max level anyone (probably KC) is - holding this sub to right now?""" - if isinstance(uri, Submaster.Submaster): - # likely the uri was spotted and replaced - uri = uri.uri - if not isinstance(uri, URIRef): - raise TypeError("got %r" % uri) - - foundLevels = list(self._currentSubSettingValues(uri)) - - if not foundLevels: - return 0 - - return max(foundLevels) - - def eval(self, songTime): - ns = {'t': songTime} - ns.update(self.otherFuncs) - - ns.update( - dict( - curve=lambda c, t: c.eval(t), - currentSubLevel=self.currentSubLevel, - )) - - # I think this is slowing effecteval. Could we cache results - # that we know haven't changed, like if a curve returns 0 - # again, we can skip an eval() call on the line that uses it - - for c in self.codes: - codeNs = ns.copy() - codeNs.update(c.pyResources) - try: - lineOut = eval(c.expr, codeNs) - except Exception as e: - e.expr = c.expr - raise e - ns[c.outName] = lineOut - if 'out' not in ns: - log.error("ran code for %s, didn't make an 'out' value", self.uri) - return ns['out'] diff -r 623836db99af -r 4556eebe5d73 light9/effecteval/effectloop.py --- a/light9/effecteval/effectloop.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,322 +0,0 @@ -import time, logging, traceback - -from rdflib import URIRef -from twisted.internet import reactor -from twisted.internet.defer import inlineCallbacks, returnValue, succeed -from twisted.internet.error import TimeoutError -import numpy -import serial -import treq - -from light9 import Effects -from light9 import Submaster -from light9 import dmxclient -from light9 import networking -from light9.effecteval.effect import EffectNode -from light9.namespaces import L9, RDF -from light9.metrics import metrics - -log = logging.getLogger('effectloop') - - -class EffectLoop: - """maintains a collection of the current EffectNodes, gets time from - music player, sends dmx""" - - def __init__(self, graph): - self.graph = graph - self.currentSong = None - self.currentEffects = [ - ] # EffectNodes for the current song plus the submaster ones - self.lastLogTime = 0 - self.lastLogMsg = "" - self.lastErrorLog = 0 - self.graph.addHandler(self.setEffects) - self.period = 1 / 30 - self.coastSecs = .3 # main reason to keep this low is to notice play/pause - self.songTimeFetch = 0 - self.songIsPlaying = False - self.songTimeFromRequest = 0 - self.requestTime = 0 # unix sec for when we fetched songTime - self.initOutput() - - def initOutput(self): - pass - - def startLoop(self): - log.info("startLoop") - self.lastSendLevelsTime = 0 - reactor.callLater(self.period, self.sendLevels) - reactor.callLater(self.period, self.updateTimeFromMusic) - - def setEffects(self): - self.currentEffects = [] - log.info('setEffects currentSong=%s', self.currentSong) - if self.currentSong is None: - return - - for effectUri in self.graph.objects(self.currentSong, L9['effect']): - self.currentEffects.append(EffectNode(self.graph, effectUri)) - - for sub in self.graph.subjects(RDF.type, L9['Submaster']): - for effectUri in self.graph.objects(sub, L9['drivesEffect']): - self.currentEffects.append(EffectNode(self.graph, effectUri)) - - log.info('now we have %s effects', len(self.currentEffects)) - - @inlineCallbacks - def getSongTime(self): - now = time.time() - old = now - self.requestTime - if old > self.coastSecs: - try: - r = yield treq.get(networking.musicPlayer.path('time'), - timeout=.5) - response = yield r.json_content() - except TimeoutError: - log.warning("TimeoutError: using stale time from %.1f ago", old) - else: - self.requestTime = now - self.currentPlaying = response['playing'] - self.songTimeFromRequest = response['t'] - returnValue((response['t'], (response['song'] and - URIRef(response['song'])))) - - estimated = self.songTimeFromRequest - if self.currentSong is not None and self.currentPlaying: - estimated += now - self.requestTime - returnValue((estimated, self.currentSong)) - - @inlineCallbacks - def updateTimeFromMusic(self): - t1 = time.time() - with metrics('get_music').time(): - self.songTime, song = yield self.getSongTime() - self.songTimeFetch = time.time() - - if song != self.currentSong: - self.currentSong = song - # this may be piling on the handlers - self.graph.addHandler(self.setEffects) - - elapsed = time.time() - t1 - reactor.callLater(max(0, self.period - elapsed), - self.updateTimeFromMusic) - - def estimatedSongTime(self): - now = time.time() - t = self.songTime - if self.currentPlaying: - t += max(0, now - self.songTimeFetch) - return t - - @inlineCallbacks - def sendLevels(self): - t1 = time.time() - log.debug("time since last call: %.1f ms" % - (1000 * (t1 - self.lastSendLevelsTime))) - self.lastSendLevelsTime = t1 - try: - with metrics('send_levels').time(): - if self.currentSong is not None: - log.debug('allEffectOutputs') - with metrics('evals').time(): - outputs = self.allEffectOutputs( - self.estimatedSongTime()) - log.debug('combineOutputs') - combined = self.combineOutputs(outputs) - self.logLevels(t1, combined) - log.debug('sendOutput') - with metrics('send_output').time(): - yield self.sendOutput(combined) - - elapsed = time.time() - t1 - dt = max(0, self.period - elapsed) - except Exception: - metrics('errors').incr() - traceback.print_exc() - dt = .5 - - reactor.callLater(dt, self.sendLevels) - - def combineOutputs(self, outputs): - """pick usable effect outputs and reduce them into one for sendOutput""" - outputs = [x for x in outputs if isinstance(x, Submaster.Submaster)] - out = Submaster.sub_maxes(*outputs) - - return out - - @inlineCallbacks - def sendOutput(self, combined): - dmx = combined.get_dmx_list() - yield dmxclient.outputlevels(dmx, twisted=True) - - def allEffectOutputs(self, songTime): - outputs = [] - for e in self.currentEffects: - try: - out = e.eval(songTime) - if isinstance(out, (list, tuple)): - outputs.extend(out) - else: - outputs.append(out) - except Exception as exc: - now = time.time() - if now > self.lastErrorLog + 5: - if hasattr(exc, 'expr'): - log.exception('in expression %r', exc.expr) - log.error("effect %s: %s" % (e.uri, exc)) - self.lastErrorLog = now - log.debug('eval %s effects, got %s outputs', len(self.currentEffects), - len(outputs)) - - return outputs - - def logLevels(self, now, out): - # this would look nice on the top of the effecteval web pages too - if log.isEnabledFor(logging.DEBUG): - log.debug(self.logMessage(out)) - else: - if now > self.lastLogTime + 5: - msg = self.logMessage(out) - if msg != self.lastLogMsg: - log.info(msg) - self.lastLogMsg = msg - self.lastLogTime = now - - def logMessage(self, out): - return ("send dmx: {%s}" % - ", ".join("%r: %.3g" % (str(k), v) - for k, v in list(out.get_levels().items()))) - - -Z = numpy.zeros((50, 3), dtype=numpy.float16) - - -class ControlBoard: - - def __init__( - self, - dev='/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A7027NYX-if00-port0' - ): - log.info('opening %s', dev) - self._dev = serial.Serial(dev, baudrate=115200) - - def _8bitMessage(self, floatArray): - px255 = (numpy.clip(floatArray, 0, 1) * 255).astype(numpy.uint8) - return px255.reshape((-1,)).tostring() - - def setStrip(self, which, pixels): - """ - which: 0 or 1 to pick the strip - pixels: (50, 3) array of 0..1 floats - """ - command = {0: '\x00', 1: '\x01'}[which] - if pixels.shape != (50, 3): - raise ValueError("pixels was %s" % pixels.shape) - self._dev.write('\x60' + command + self._8bitMessage(pixels)) - self._dev.flush() - - def setUv(self, which, level): - """ - which: 0 or 1 - level: 0 to 1 - """ - command = {0: '\x02', 1: '\x03'}[which] - self._dev.write('\x60' + command + - chr(int(max(0, min(1, level)) * 255))) - self._dev.flush() - - def setRgb(self, color): - """ - color: (1, 3) array of 0..1 floats - """ - if color.shape != (1, 3): - raise ValueError("color was %s" % color.shape) - self._dev.write('\x60\x04' + self._8bitMessage(color)) - self._dev.flush() - - -class LedLoop(EffectLoop): - - def initOutput(self): - self.board = ControlBoard() - self.lastSent = {} # what's in arduino's memory - - def combineOutputs(self, outputs): - combined = { - 'L': Z, - 'R': Z, - 'blacklight0': 0, - 'blacklight1': 0, - 'W': numpy.zeros((1, 3), dtype=numpy.float16) - } - - for out in outputs: - log.debug('combine output %r', out) - - # workaround- somehow these subs that drive fx aren't - # sending their fx during playback (KC only), so we react - # to the sub itself - if isinstance(out, Submaster.Submaster) and '*' in out.name: - level = float(out.name.split('*')[1]) - n = out.name.split('*')[0] - if n == 'widered': - out = Effects.Strip.solid('LRW', (1, 0, 0)) * level - if n == 'widegreen': - out = Effects.Strip.solid('LRW', (0, 1, 0)) * level - if n == 'wideblue': - out = Effects.Strip.solid('LRW', (0, 0, 1)) * level - if n == 'whiteled': - out = Effects.Strip.solid('LRW', (1, .7, .7)) * level - if n == 'blacklight': - out = Effects.Blacklight(level) # missing blues! - - if isinstance(out, Effects.Blacklight): - # no picking yet - #key = 'blacklight%s' % out.which - for key in ['blacklight0', 'blacklight1']: - combined[key] = max(combined[key], out) - elif isinstance(out, Effects.Strip): - pixels = numpy.array(out.pixels, dtype=numpy.float16) - for w in out.which: - combined[w] = numpy.maximum( - combined[w], pixels[:1, :] if w == 'W' else pixels) - - return combined - - @inlineCallbacks - def sendOutput(self, combined): - for meth, selectArgs, value in [ - ('setStrip', (0,), combined['L']), - ('setStrip', (1,), combined['R']), - ('setUv', (0,), combined['blacklight0']), - ('setUv', (1,), combined['blacklight1']), - ('setRgb', (), combined['W']), - ]: - key = (meth, selectArgs) - compValue = value.tolist() if isinstance(value, - numpy.ndarray) else value - - if self.lastSent.get(key) == compValue: - continue - - log.debug('value changed: %s(%s %s)', meth, selectArgs, value) - - getattr(self.board, meth)(*(selectArgs + (value,))) - self.lastSent[key] = compValue - - yield succeed(None) # there was an attempt at an async send - - def logMessage(self, out): - return str([(w, p.tolist() if isinstance(p, numpy.ndarray) else p) - for w, p in list(out.items())]) - - -def makeEffectLoop(graph, outputWhere): - if outputWhere == 'dmx': - return EffectLoop(graph) - elif outputWhere == 'leds': - return LedLoop(graph) - else: - raise NotImplementedError("unknown output system %r" % outputWhere) diff -r 623836db99af -r 4556eebe5d73 light9/effecteval/index.html --- a/light9/effecteval/index.html Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,16 +0,0 @@ - - - - effecteval - - - - - -
starting...
-

Effect instances [stats]

- - - - - diff -r 623836db99af -r 4556eebe5d73 light9/effecteval/test_effect.py --- a/light9/effecteval/test_effect.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,83 +0,0 @@ -import unittest -import mock -import sys -sys.path.insert(0, 'bin') # for run_local - -from .effect import CodeLine -from rdflib import URIRef - - -def isCurve(self, uri): - return 'curve' in uri - - -def isSub(self, uri): - return 'sub' in uri - - -@mock.patch('light9.effecteval.effect.CodeLine._uriIsCurve', new=isCurve) -@mock.patch('light9.effecteval.effect.CodeLine._uriIsSub', new=isSub) -@mock.patch('light9.effecteval.effect.CodeLine._resourcesAsPython', - new=lambda self, r: self.expr) -class TestAsPython(unittest.TestCase): - - def test_gets_lname(self): - ec = CodeLine(graph=None, code='x = y+1') - self.assertEqual('x', ec.outName) - - def test_gets_simple_code(self): - ec = CodeLine(graph=None, code='x = y+1') - self.assertEqual('y+1', ec._asPython()[2]) - self.assertEqual({}, ec._asPython()[3]) - - def test_converts_uri_to_var(self): - ec = CodeLine(graph=None, code='x = ') - _, inExpr, expr, uris = ec._asPython() - self.assertEqual('_res0', expr) - self.assertEqual({'_res0': URIRef('http://example.com/')}, uris) - - def test_converts_multiple_uris(self): - ec = CodeLine(graph=None, - code='x = + ') - _, inExpr, expr, uris = ec._asPython() - self.assertEqual('_res0 + _res1', expr) - self.assertEqual( - { - '_res0': URIRef('http://example.com/'), - '_res1': URIRef('http://other') - }, uris) - - def test_doesnt_fall_for_brackets(self): - ec = CodeLine(graph=None, code='x = 1<2>3< h') - _, inExpr, expr, uris = ec._asPython() - self.assertEqual('1<2>3< h', expr) - self.assertEqual({}, uris) - - def test_curve_uri_expands_to_curve_eval_func(self): - ec = CodeLine(graph=None, code='x = ') - _, inExpr, expr, uris = ec._asPython() - self.assertEqual('curve(_res0, t)', expr) - self.assertEqual({'_res0': URIRef('http://example/curve1')}, uris) - - def test_curve_doesnt_double_wrap(self): - ec = CodeLine(graph=None, - code='x = curve(, t+.01)') - _, inExpr, expr, uris = ec._asPython() - self.assertEqual('curve(_res0, t+.01)', expr) - self.assertEqual({'_res0': URIRef('http://example/curve1')}, uris) - - -@mock.patch('light9.effecteval.effect.CodeLine._uriIsCurve', new=isCurve) -@mock.patch('light9.effecteval.effect.CodeLine._resourcesAsPython', - new=lambda self, r: self.expr) -class TestPossibleVars(unittest.TestCase): - - def test1(self): - self.assertEqual(set([]), CodeLine(None, 'a1 = 1').possibleVars) - - def test2(self): - self.assertEqual({'a2'}, CodeLine(None, 'a1 = a2').possibleVars) - - def test3(self): - self.assertEqual({'a2', 'a3'}, - CodeLine(None, 'a1 = a2 + a3').possibleVars) diff -r 623836db99af -r 4556eebe5d73 light9/gtkpyconsole.py --- a/light9/gtkpyconsole.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,38 +0,0 @@ -from lib.ipython_view import IPythonView -import gi # noqa -from gi.repository import Gtk -from gi.repository import Pango - - -def togglePyConsole(self, item, user_ns): - """ - toggles a toplevel window with an ipython console inside. - - self is an object we can stick the pythonWindow attribute on - - item is a checkedmenuitem - - user_ns is a dict you want to appear as locals in the console - """ - if item.get_active(): - if not hasattr(self, 'pythonWindow'): - self.pythonWindow = Gtk.Window() - S = Gtk.ScrolledWindow() - S.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) - V = IPythonView(user_ns=user_ns) - V.modify_font(Pango.FontDescription("luxi mono 8")) - V.set_wrap_mode(Gtk.WrapMode.CHAR) - S.add(V) - self.pythonWindow.add(S) - self.pythonWindow.show_all() - self.pythonWindow.set_size_request(750, 550) - self.pythonWindow.set_resizable(True) - - def onDestroy(*args): - item.set_active(False) - del self.pythonWindow - - self.pythonWindow.connect("destroy", onDestroy) - else: - if hasattr(self, 'pythonWindow'): - self.pythonWindow.destroy() diff -r 623836db99af -r 4556eebe5d73 light9/homepage/write_config.py --- a/light9/homepage/write_config.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,89 +0,0 @@ -''' - - goal (everything under localhost:8200): - / light9/web/index.html - /effects light9/web/effects/index.html - /collector/ light9/web/collector/index.html - /show/dance2023/URI light9/show/dance2023/URI - /service/collector/ localhost:8302 - /service/collector/metrics localhost:8302/metrics -''' -import sys -from pathlib import Path -from urllib.parse import urlparse - -from light9 import showconfig -from light9.namespaces import L9 -from light9.run_local import log - - -def main(): - [outPath] = sys.argv[1:] - - log.info(f'writing nginx config to {outPath}') - graph = showconfig.getGraph() - netHome = graph.value(showconfig.showUri(), L9['networking']) - webServer = graph.value(netHome, L9['webServer']) - if not webServer: - raise ValueError('no %r :webServer' % netHome) - mime_types = Path(__file__).parent.parent / 'web/mime.types' - nginx_port = urlparse(str(webServer)).port - with open(outPath, 'wt') as out: - print(f''' -worker_processes 1; - -daemon off; -error_log /tmp/light9_homepage.err; -pid /dev/null; - -events {{ - worker_connections 1024; -}} - -http {{ - include {mime_types}; - - proxy_buffering off; - proxy_http_version 1.1; - - # for websocket - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - server {{ - listen {nginx_port}; - access_log off; - autoindex on;''', - file=out) - - for role, server in sorted(graph.predicate_objects(netHome)): - if not str(server).startswith('http') or role == L9['webServer']: - continue - path = graph.value(role, L9['urlPath']) - if not path: - continue - server = str(server).rstrip('/') - print(f''' - location = /{path} {{ rewrite (.*) $1/ permanent; }} - location /service/{path}/ {{ - rewrite ^/service/{path}(/.*) $1 break; - proxy_pass {server}; - }}''', - file=out) - - showPath = showconfig.showUri().split('/', 3)[-1] - root = showconfig.root()[:-len(showPath)].decode('ascii') - print(f''' - location /show/ {{ - root {root}; - }} - - location / {{ - proxy_pass http://localhost:8300; - }} - }} -}}''', file=out) - - -if __name__ == '__main__': - main() \ No newline at end of file diff -r 623836db99af -r 4556eebe5d73 light9/homepage/write_config_test.py --- a/light9/homepage/write_config_test.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,3 +0,0 @@ -def test_import(): - import write_config - # no crash diff -r 623836db99af -r 4556eebe5d73 light9/io/Makefile --- a/light9/io/Makefile Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ -LIB=/usr/local/lib -INC=-I/usr/local/include/python2.3 - -go: _parport.so _serport.so - result="your modules and links are now up to date" - -_parport.so: parport_wrap.c - gcc -shared -g ${INC} parport_wrap.c parport.c -o _parport.so - -parport_wrap.c: parport.c parport.i - swig -python parport.i - -_serport.so: serport_wrap.c - gcc -shared -O ${INC} serport_wrap.c -o _serport.so - -serport_wrap.c: serport.i - swig -python serport.i - -clean: - rm -f parport_wrap.c serport_wrap.c *.o *.so diff -r 623836db99af -r 4556eebe5d73 light9/io/__init__.py --- a/light9/io/__init__.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,89 +0,0 @@ -import sys - - -class BaseIO(object): - - def __init__(self): - self.dummy = 1 - self.__name__ = 'BaseIO' - # please override and set __name__ to your class name - - def golive(self): - """call this if you want to promote the dummy object becomes a live object""" - print("IO: %s is going live" % self.__name__) - self.dummy = 0 - # you'd override with additional startup stuff here, - # perhaps even loading a module and saving it to a class - # attr so the subclass-specific functions can use it - - def godummy(self): - print("IO: %s is going dummy" % self.__name__) - self.dummy = 1 - # you might override this to close ports, etc - - def isdummy(self): - return self.dummy - - def __repr__(self): - if self.dummy: - return "" % self.__name__ - else: - return "" % self.__name__ - - # the derived class will have more methods to do whatever it does, - # and they should return dummy values if self.dummy==1. - - -class ParportDMX(BaseIO): - - def __init__(self, dimmers=68): - BaseIO.__init__(self) - self.__name__ = 'ParportDMX' - self.dimmers = dimmers - - def golive(self): - BaseIO.golive(self) - from . import parport - self.parport = parport - self.parport.getparport() - - def sendlevels(self, levels): - if self.dummy: - return - - levels = list(levels) + [0] - # if levels[14] > 0: levels[14] = 100 # non-dim - self.parport.outstart() - for p in range(1, self.dimmers + 2): - self.parport.outbyte(levels[p - 1] * 255 / 100) - - -class UsbDMX(BaseIO): - - def __init__(self, dimmers=72, port='/dev/dmx0'): - BaseIO.__init__(self) - self.__name__ = "UsbDMX" - self.port = port - self.out = None - self.dimmers = dimmers - - def _dmx(self): - if self.out is None: - if self.port == 'udmx': - from .udmx import Udmx - self.out = Udmx() - self.out.write = self.out.SendDMX - else: - sys.path.append("dmx_usb_module") - from dmx import Dmx - self.out = Dmx(self.port) - return self.out - - def sendlevels(self, levels): - if self.dummy: - return - # I was outputting on 76 and it was turning on the light at - # dmx75. So I added the 0 byte. - packet = '\x00' + ''.join([chr(int(lev * 255 / 100)) for lev in levels - ]) + "\x55" - self._dmx().write(packet) diff -r 623836db99af -r 4556eebe5d73 light9/io/motordrive --- a/light9/io/motordrive Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,144 +0,0 @@ -#!/usr/bin/python - -from __future__ import division -from twisted.internet import reactor, tksupport -import Tkinter as tk -import time, atexit -from louie import dispatcher -import parport -parport.getparport() - - -class Ctl: - def __init__(self): - self.blade = False - self.xpos = 0 - self.ypos = 0 - - dispatcher.connect(self.dragTo, "dragto") - self.path = [] # future points to walk to - self.lastByteTime = 0 - - def dragTo(self, x, y): - self.path.append((x,y)) - #print "drag to", x, y, len(self.path) - dispatcher.send("new path", path=self.path) - - def step(self): - start = time.time() - while time.time() - start < .05: - self._step() - - def _step(self): - if not self.path: - return - goal = self.path[0] - if (self.xpos, self.ypos) == goal: - self.path.pop(0) - dispatcher.send("new path", path=self.path) - return - self.move(cmp(goal[0], self.xpos), - cmp(goal[1], self.ypos)) - - def move(self, dx, dy): - self.xpos += dx - self.ypos += dy - dispatcher.send("coords", x=self.xpos, y=self.ypos) - #print "x=%d y=%d" % (self.xpos, self.ypos) - self.update() - - def update(self): - byte = 0 - if self.blade: - byte |= 0x80 - - byte |= (0x01, 0x03, 0x02, 0x00)[self.xpos % 4] * 0x20 - byte |= (0x01, 0x03, 0x02, 0x00)[self.ypos % 4] * 0x04 - - byte |= 0x01 # power pin - byte |= 0x02 | 0x10 # enable dirs - - now = time.time() - print "%.1fms delay between bytes" % ((now - self.lastByteTime) * 1000) - self.out(byte) - self.lastByteTime = now - - def out(self, byte): - #print hex(byte) - parport.outdata(byte) - time.sleep(.003) - - def toggleBlade(self): - self.blade = not self.blade - if self.blade: - # blade needs full power to go down - self.out(0x80) - time.sleep(.05) - self.update() - -class Canv(tk.Canvas): - def __init__(self, master, **kw): - tk.Canvas.__init__(self, master, **kw) - self.create_line(0,0,0,0, tags='cursorx') - self.create_line(0,0,0,0, tags='cursory') - dispatcher.connect(self.updateCursor, "coords") - dispatcher.connect(self.drawPath, "new path") - self.bind("", self.b1motion) - - def canFromWorld(self, wx, wy): - return -wx / 5 + 300, wy / 5 + 300 - - def worldFromCan(self, cx, cy): - return -(cx - 300) * 5, (cy - 300) * 5 - - def updateCursor(self, x, y): - x,y = self.canFromWorld(x, y) - self.coords('cursorx', x-10, y, x+10, y) - self.coords('cursory', x, y-10, x, y+10) - - def b1motion(self, ev): - wx,wy = self.worldFromCan(ev.x, ev.y) - dispatcher.send("dragto", x=wx, y=wy) - - def drawPath(self, path): - self.delete('path') - pts = [] - for pt in path: - pts.extend(self.canFromWorld(*pt)) - if len(pts) >= 4: - self.create_line(*pts, **dict(tag='path')) - -root = tk.Tk() - - -ctl = Ctl() - -can = Canv(root, width=900, height=900) -can.pack() - -for key, byte in [ - ('0', 0), - ]: - root.bind("" % key, lambda ev, byte=byte: ctl.out(byte)) - -for key, xy in [('Left', (-1, 0)), - ('Right', (1, 0)), - ('Up', (0, -1)), - ('Down', (0, 1))]: - root.bind("" % key, lambda ev, xy=xy: ctl.move(*xy)) - -root.bind("", lambda ev: ctl.toggleBlade()) - -ctl.move(0,0) - -atexit.register(lambda: ctl.out(0)) - -def loop(): - ctl.step() - root.after(10, loop) -loop() - -tksupport.install(root, ms=5) -root.protocol('WM_DELETE_WINDOW', reactor.stop) -reactor.run() - diff -r 623836db99af -r 4556eebe5d73 light9/io/parport.c --- a/light9/io/parport.c Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,58 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -int getparport() { - printf("parport - ver 4\n"); - if( ioperm(888,3,1) ) { - printf("Couldn't get parallel port at 888-890\n"); - - // the following doesn't have any effect! - PyErr_SetString(PyExc_IOError,"Couldn't get parallel port at 888-890"); - return 0; - } - return 1; -} - -void outdata(unsigned char val) { - outb(val,888); -} - -void outcontrol( unsigned char val ) { - outb(val,890); -} - -void outbyte( unsigned char val ) { - int i; - // set data, raise clock, lower clock - outdata(val); - - /* this was originally 26 outcontrol calls, but on new dash that - leads to screwed up dmx about once a minute. I tried doing 26*4 - outcontrol calls, but it still screwed up. I suspect the athlon64 - or my new kernel version is sending the parport really fast, - sometimes faster than the pic sees the bits. Then I put a 1ms - sleep after the outcontrol(2)'s and that didn't help either, so - I'm not sure what's going on. Putting the parallel cable on miles - seems to work. - - todo: - try a little pause after outcontrol(3) to make sure pic sees that - */ - - for (i=0; i<26*4; i++) { - outcontrol(2); - } - outcontrol(3); -} -void outstart() { - // send start code: pin 14 high, 5ms to let a dmx cycle finish, - // then pin14 low (pin1 stays low) - outcontrol(1); - usleep(5000); - outcontrol(3); -} diff -r 623836db99af -r 4556eebe5d73 light9/io/parport.i --- a/light9/io/parport.i Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,11 +0,0 @@ -%module parport - - -extern void getparport(); -extern void outdata( unsigned char val); -extern void outcontrol( unsigned char val ); -extern void outbyte( unsigned char val ); -extern void outstart(); - - - diff -r 623836db99af -r 4556eebe5d73 light9/io/serport.i --- a/light9/io/serport.i Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,50 +0,0 @@ -%module serport - -%{ -#include -#include -#include -#include -#include -#include -#include -%} - - -%typemap(python,in) __u8 { - if( !PyInt_Check($input)) { - PyErr_SetString(PyExc_TypeError,"not an integer"); - return NULL; - } - $1 = ($type) PyInt_AsLong($input); -} - -%typemap(python,out) __s32 { - $result = Py_BuildValue("i", ($type) $1); -} - -%inline %{ - - __s32 i2c_smbus_write_byte(int file, __u8 value); - __s32 i2c_smbus_read_byte(int file); - - PyObject *read_all_adc(int file) { - PyObject *t=PyTuple_New(4); - - #define CHAN_TO_TUPLE_POS(chan,idx) i2c_smbus_write_byte(file, chan);\ - PyTuple_SetItem(t,idx,PyInt_FromLong(i2c_smbus_read_byte(file))); - - /* - these are shuffled here to match the way the pots read in. in - the returned tuple, 0=left pot..3=right pot. - */ - CHAN_TO_TUPLE_POS(1,0) - CHAN_TO_TUPLE_POS(2,1) - CHAN_TO_TUPLE_POS(3,2) - CHAN_TO_TUPLE_POS(0,3) - - return t; - - } - -%} diff -r 623836db99af -r 4556eebe5d73 light9/io/udmx.py --- a/light9/io/udmx.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,62 +0,0 @@ -import logging -import usb.core -from usb.util import CTRL_TYPE_VENDOR, CTRL_RECIPIENT_DEVICE, CTRL_OUT - -log = logging.getLogger('udmx') -""" -Send dmx to one of these: -http://www.amazon.com/Interface-Adapter-Controller-Lighting-Freestyler/dp/B00W52VIOS - -[4520784.059479] usb 1-2.3: new low-speed USB device number 6 using xhci_hcd -[4520784.157410] usb 1-2.3: New USB device found, idVendor=16c0, idProduct=05dc -[4520784.157416] usb 1-2.3: New USB device strings: Mfr=1, Product=2, SerialNumber=3 -[4520784.157419] usb 1-2.3: Product: uDMX -[4520784.157422] usb 1-2.3: Manufacturer: www.anyma.ch -[4520784.157424] usb 1-2.3: SerialNumber: ilLUTZminator001 - -See https://www.illutzmination.de/udmxfirmware.html?&L=1 - sources/commandline/uDMX.c -or https://github.com/markusb/uDMX-linux/blob/master/uDMX.c -""" - -cmd_SetChannelRange = 0x0002 - - -class Udmx: - - def __init__(self, bus): - self.dev = None - for dev in usb.core.find(idVendor=0x16c0, - idProduct=0x05dc, - find_all=True): - print("udmx device at %r" % dev.bus) - if bus is None or bus == dev.bus: - self.dev = dev - if not self.dev: - raise IOError('no matching udmx device found for requested bus %r' % - bus) - log.info('found udmx at %r', self.dev) - - def SendDMX(self, buf): - ret = self.dev.ctrl_transfer(bmRequestType=CTRL_TYPE_VENDOR | - CTRL_RECIPIENT_DEVICE | CTRL_OUT, - bRequest=cmd_SetChannelRange, - wValue=len(buf), - wIndex=0, - data_or_wLength=buf) - if ret < 0: - raise ValueError("ctrl_transfer returned %r" % ret) - - -def demo(chan, fps=44): - import time, math - u = Udmx() - while True: - nsin = math.sin(time.time() * 6.28) / 2.0 + 0.5 - nsin8 = int(255 * nsin) - try: - u.SendDMX('\x00' * (chan - 1) + chr(210) + chr(nsin8) + chr(nsin8) + - chr(nsin8)) - except usb.core.USBError as e: - print("err", time.time(), repr(e)) - time.sleep(1 / fps) diff -r 623836db99af -r 4556eebe5d73 light9/localsyncedgraph.py --- a/light9/localsyncedgraph.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,19 +0,0 @@ -from rdflib import ConjunctiveGraph - -from rdfdb.syncedgraph.currentstategraphapi import CurrentStateGraphApi -from rdfdb.syncedgraph.autodepgraphapi import AutoDepGraphApi -from rdfdb.syncedgraph.grapheditapi import GraphEditApi -from rdfdb.rdflibpatch import patchQuads - - -class LocalSyncedGraph(AutoDepGraphApi, GraphEditApi): - """for tests""" - - def __init__(self, files=None): - self._graph = ConjunctiveGraph() - for f in files or []: - self._graph.parse(f, format='n3') - - def patch(self, p): - patchQuads(self._graph, deleteQuads=p.delQuads, addQuads=p.addQuads, perfect=True) - # no deps diff -r 623836db99af -r 4556eebe5d73 light9/metrics.py --- a/light9/metrics.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,120 +0,0 @@ -"""for easier porting, and less boilerplate, allow these styles using the -form of the call to set up the right type of metric automatically: - - from metrics import metrics - metrics.setProcess('pretty_name') - - @metrics('loop').time() # a common one to get the fps of each service. Gets us qty and time - def frame(): - if err: - metrics('foo_errors').incr() # if you incr it, it's a counter - - @metrics('foo_calls').time() # qty & time because it's a decorator - def foo(): - - metrics('goal_fps').set(f) # a gauge because we called set() - - with metrics('recompute'): ... # ctxmgr also makes a timer - time_this_part() - -I don't see a need for labels yet, but maybe some code will want like -metrics('foo', label1=one). Need histogram? Info? - -""" -from typing import Dict, Tuple, Callable, Type, TypeVar, cast -from prometheus_client import Counter, Gauge, Metric, Summary -from prometheus_client.exposition import generate_latest -from prometheus_client.registry import REGISTRY - -_created: Dict[str, Metric] = {} - -# _process=sys.argv[0] -# def setProcess(name: str): -# global _process -# _process = name - -MT = TypeVar("MT") - - -class _MetricsRequest: - - def __init__(self, name: str, **labels): - self.name = name - self.labels = labels - - def _ensure(self, cls: Type[MT]) -> MT: - if self.name not in _created: - _created[self.name] = cls(name=self.name, documentation=self.name, labelnames=self.labels.keys()) - m = _created[self.name] - if self.labels: - m = m.labels(**self.labels) - return m - - def __call__(self, fn) -> Callable: - return timed_fn - - def set(self, v: float): - self._ensure(Gauge).set(v) - - def inc(self): - self._ensure(Counter).inc() - - def offset(self, amount: float): - self._ensure(Gauge).inc(amount) - - def time(self): - return self._ensure(Summary).time() - - def observe(self, x: float): - return self._ensure(Summary).observe(x) - - def __enter__(self): - return self._ensure(Summary).__enter__() - - -def metrics(name: str, **labels): - return _MetricsRequest(name, **labels) - - - - -""" -stuff we used to have in greplin. Might be nice to get (client-side-computed) min/max/stddev back. - -class PmfStat(Stat): - A stat that stores min, max, mean, standard deviation, and some - percentiles for arbitrary floating-point data. This is potentially a - bit expensive, so its child values are only updated once every - twenty seconds. - - - - - -i think prometheus covers this one: - -import psutil -def gatherProcessStats(): - procStats = scales.collection('/process', - scales.DoubleStat('time'), - scales.DoubleStat('cpuPercent'), - scales.DoubleStat('memMb'), - ) - proc = psutil.Process() - lastCpu = [0.] - def updateTimeStat(): - now = time.time() - procStats.time = round(now, 3) - if now - lastCpu[0] > 3: - procStats.cpuPercent = round(proc.cpu_percent(), 6) # (since last call) - lastCpu[0] = now - procStats.memMb = round(proc.memory_info().rss / 1024 / 1024, 6) - task.LoopingCall(updateTimeStat).start(.1) - -""" - - -class M: - - def __call__(self, name): - return diff -r 623836db99af -r 4556eebe5d73 light9/midifade/midifade.py --- a/light9/midifade/midifade.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,240 +0,0 @@ -#!bin/python -""" -Read midi events, write fade levels to graph -""" -import asyncio -import logging -import traceback -from typing import Dict, List, cast -from light9.effect.edit import clamp - -import mido -from rdfdb.syncedgraph.syncedgraph import SyncedGraph -from rdflib import RDF, ConjunctiveGraph, Literal, URIRef -from rdfdb.syncedgraph.readonly_graph import ReadOnlyConjunctiveGraph -from light9 import networking -from light9.namespaces import L9 -from light9.newtypes import decimalLiteral -from light9.run_local import log -from light9.showconfig import showUri - -mido.set_backend('alsa_midi.mido_backend') -MAX_SEND_RATE = 30 - -_lastSet = {} #midictlchannel:value7bit - -currentFaders = {} # midi control channel num : FaderUri -ctx = URIRef(showUri() + '/fade') - - -def compileCurrents(graph): - currentFaders.clear() - try: - new = getChansToFaders(graph) - except ValueError: - return # e.g. empty-graph startup - currentFaders.update(new) - - -def getGraphMappingNode(g: ReadOnlyConjunctiveGraph | SyncedGraph) -> URIRef: - mapping = g.value(L9['midiControl'], L9['map']) - if mapping is None: - raise ValueError('no :midiControl :map ?mapping') - midiDev = g.value(mapping, L9['midiDev']) - ourDev = 'bcf2000' - if midiDev != Literal(ourDev): - raise NotImplementedError(f'need {mapping} to have :midiDev {ourDev!r}') - return mapping - - -def getCurMappedPage(g: SyncedGraph): - mapping = getGraphMappingNode(g) - return g.value(mapping, L9['outputs']) - - -def setCurMappedPage(g: SyncedGraph, mapping: URIRef, newPage: URIRef): - g.patchObject(ctx, mapping, L9.outputs, newPage) - - -def getChansToFaders(g: SyncedGraph) -> Dict[int, URIRef]: - fadePage = getCurMappedPage(g) - ret = [] - for f in g.objects(fadePage, L9.fader): - columnLit = cast(Literal, g.value(f, L9['column'])) - col = int(columnLit.toPython()) - ret.append((col, f)) - - ret.sort() - ctl_channels = list(range(81, 88 + 1)) - out = {} - for chan, (col, f) in zip(ctl_channels, ret): - out[chan] = f - return out - - -def changePage(g: SyncedGraph, dp: int): - """dp==-1, make the previous page active, etc. Writes to graph""" - - with g.currentState() as current: - allPages = sorted(current.subjects(RDF.type, L9.FadePage), key=lambda fp: str(fp)) - mapping = getGraphMappingNode(current) - curPage = current.value(mapping, L9.outputs) - if curPage is None: - curPage = allPages[0] - idx = allPages.index(curPage) - newIdx = clamp(idx + dp, 0, len(allPages) - 1) - print('change from ', idx, newIdx) - newPage = allPages[newIdx] - setCurMappedPage(g, mapping, newPage) - - -def writeHwValueToGraph(graph: SyncedGraph, ctx, fader: URIRef, strength: float): - log.info(f'setFader(fader={fader}, strength={strength:.03f}') - valueLit = decimalLiteral(round(strength, 3)) - with graph.currentState() as g: - fadeSet = g.value(fader, L9['setting']) - if fadeSet is None: - raise ValueError(f'fader {fader} has no :setting') - graph.patchObject(ctx, fadeSet, L9['value'], valueLit) - - -def changeGrandMaster(graph: SyncedGraph, newValue: float): - graph.patchObject(ctx, L9.grandMaster, L9['value'], decimalLiteral(newValue)) - - -def onMessage(graph: SyncedGraph, ctx: URIRef, m: Dict): - if m['type'] == 'active_sensing': - return - if m['type'] == 'control_change': - if m['dev'] == 'bcf2000' and m['control'] == 91: - changePage(graph, -1) - return - if m['dev'] == 'bcf2000' and m['control'] == 92: - changePage(graph, 1) - return - if m['dev'] == 'bcf2000' and m['control'] == 8: - changeGrandMaster(graph, clamp(m['value'] / 127 * 1.5, 0, 1)) - return - - try: - fader = { - 'quneo': { - 44: L9['show/dance2023/fadePage1f0'], - 45: L9['show/dance2023/fadePage1f0'], - 46: L9['show/dance2023/fadePage1f0'], - }, - 'bcf2000': currentFaders, - }[m['dev']][m['control']] - except KeyError: - log.info(f'unknown control {m}') - return - try: - writeHwValueToGraph(graph, ctx, fader, m['value'] / 127) - _lastSet[m['control']] = m['value'] - except ValueError as e: - log.warning(f'{e!r} - ignoring') - else: - log.info(f'unhandled message {m}') - - -def reduceToLatestValue(ms: List[Dict]) -> List[Dict]: - merge = {} - for m in ms: - normal_key = tuple(sorted(dict((k, v) for k, v in m.items() if k != 'value'))) - merge[normal_key] = m - return merge.values() - - -class WriteBackFaders: - - def __init__(self, graph: SyncedGraph, bcf_out, getCurrentValue): - self.graph = graph - self.bcf_out = bcf_out - self.getCurrentValue = getCurrentValue - - def update(self): - try: - self._update() - except ValueError as e: - log.warning(repr(e)) - - def _update(self): - g = self.graph - nupdated = 0 - m = getChansToFaders(g) - for midi_ctl_addr, f in m.items(): - fset = g.value(f, L9.setting) - # could split this to a separate handler per fader - value = g.value(fset, L9.value).toPython() - hwcurrent = self.getCurrentValue(midi_ctl_addr) - hwgoal = int(value * 127) - print(f'{f} {hwcurrent=} {hwgoal=}') - if abs(hwcurrent - hwgoal) > 2: - self.sendToBcf(midi_ctl_addr, hwgoal) - nupdated += 1 - log.info(f'wrote to {nupdated} of {len(m)} mapped faders') - - def sendToBcf(self, control, value): - _lastSet[control] = value - msg = mido.Message('control_change', control=control, value=value) - self.bcf_out.send(msg) - - -async def main(): - logging.getLogger('autodepgraphapi').setLevel(logging.INFO) - logging.getLogger('syncedgraph').setLevel(logging.INFO) - logging.getLogger('graphedit').setLevel(logging.INFO) - - graph = SyncedGraph(networking.rdfdb.url, "midifade") - ctx = URIRef(showUri() + '/fade') - - msgs = asyncio.Queue() - loop = asyncio.get_event_loop() - - def onMessageMidoThread(dev, message): - loop.call_soon_threadsafe(msgs.put_nowait, message.dict() | {'dev': dev}) - - async def reader(): - while True: - recents = [await msgs.get()] - while not msgs.empty(): - recents.append(msgs.get_nowait()) - try: - for msg in reduceToLatestValue(recents): - onMessage(graph, ctx, msg) - except Exception as e: - traceback.print_exc() - log.warning("error in onMessage- continuing anyway") - await asyncio.sleep(1 / MAX_SEND_RATE) - - asyncio.create_task(reader()) - openPorts = [] - for inputName in mido.get_input_names(): # type: ignore - if inputName.startswith('Keystation'): - dev = "keystation" - elif inputName.startswith('BCF2000'): - dev = 'bcf2000' - elif inputName.startswith('QUNEO'): - dev = 'quneo' - else: - continue - log.info(f'listening on input {inputName} {dev=}') - openPorts.append(mido.open_input( # type: ignore - inputName, # - callback=lambda message, dev=dev: onMessageMidoThread(dev, message))) - - graph.addHandler(lambda: compileCurrents(graph)) - - for outputName in mido.get_output_names(): # type: ignore - if outputName.startswith('BCF2000'): - bcf_out = mido.open_output(outputName) # type: ignore - wb = WriteBackFaders(graph, bcf_out, getCurrentValue=lambda f: _lastSet.get(f, 0)) - graph.addHandler(wb.update) - break - - while True: - await asyncio.sleep(1) - - -if __name__ == '__main__': - asyncio.run(main()) diff -r 623836db99af -r 4556eebe5d73 light9/midifade/midifade_test.py --- a/light9/midifade/midifade_test.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,5 +0,0 @@ -from light9.run_local import log - - -def test_import(): - import light9.midifade.midifade diff -r 623836db99af -r 4556eebe5d73 light9/mock_syncedgraph.py --- a/light9/mock_syncedgraph.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,59 +0,0 @@ -from rdflib import Graph, RDF, RDFS -from rdflib.parser import StringInputSource -from rdfdb.syncedgraph.syncedgraph import SyncedGraph - - -class MockSyncedGraph(SyncedGraph): - """ - Lets users of SyncedGraph mostly work. Doesn't yet help with any - testing of the rerun-upon-graph-change behavior. - """ - - def __init__(self, n3Content): - self._graph = Graph() - self._graph.parse(StringInputSource(n3Content), format='n3') - - def addHandler(self, func): - func() - - def value(self, - subject=None, - predicate=RDF.value, - object=None, - default=None, - any=True): - if object is not None: - raise NotImplementedError() - return self._graph.value(subject, - predicate, - object=object, - default=default, - any=any) - - def objects(self, subject=None, predicate=None): - return self._graph.objects(subject, predicate) - - def label(self, uri): - return self.value(uri, RDFS.label) - - def subjects(self, predicate=None, object=None): - return self._graph.subjects(predicate, object) - - def predicate_objects(self, subject): - return self._graph.predicate_objects(subject) - - def items(self, listUri): - """generator. Having a chain of watchers on the results is not - well-tested yet""" - chain = set([listUri]) - while listUri: - item = self.value(listUri, RDF.first) - if item: - yield item - listUri = self.value(listUri, RDF.rest) - if listUri in chain: - raise ValueError("List contains a recursive rdf:rest reference") - chain.add(listUri) - - def contains(self, triple): - return triple in self._graph diff -r 623836db99af -r 4556eebe5d73 light9/namespaces.py --- a/light9/namespaces.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,25 +0,0 @@ -from rdflib import URIRef, Namespace, RDF, RDFS # noqa -from typing import Dict - - -# Namespace was showing up in profiles -class FastNs: - - def __init__(self, base): - self.ns = Namespace(base) - self.cache: Dict[str, URIRef] = {} - - def __getitem__(self, term) -> URIRef: - if term not in self.cache: - self.cache[term] = self.ns[term] - return self.cache[term] - - __getattr__ = __getitem__ - - -L9 = FastNs("http://light9.bigasterisk.com/") -FUNC = FastNs("http://light9.bigasterisk.com/effectFunction/") -MUS = Namespace("http://light9.bigasterisk.com/music/") -XSD = Namespace("http://www.w3.org/2001/XMLSchema#") -DCTERMS = Namespace("http://purl.org/dc/terms/") -DEV = Namespace("http://light9.bigasterisk.com/device/") diff -r 623836db99af -r 4556eebe5d73 light9/networking.py --- a/light9/networking.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,61 +0,0 @@ -from urllib.parse import urlparse - -from rdflib import URIRef - -from .showconfig import getGraph, showUri -from .namespaces import L9 - - -class ServiceAddress: - - def __init__(self, service): - self.service = service - - def _url(self) -> URIRef: - graph = getGraph() - net = graph.value(showUri(), L9['networking']) - ret = graph.value(net, self.service) - if ret is None: - raise ValueError("no url for %s -> %s -> %s" % - (showUri(), L9['networking'], self.service)) - assert isinstance(ret, URIRef) - return ret - - @property - def port(self): - return urlparse(self._url()).port - - @property - def host(self): - return urlparse(self._url()).hostname - - @property - def url(self) -> URIRef: - return self._url() - - value = url - - def path(self, more: str) -> URIRef: - return URIRef(self.url + more) - - -captureDevice = ServiceAddress(L9['captureDevice']) -curveCalc = ServiceAddress(L9['curveCalc']) -dmxServer = ServiceAddress(L9['dmxServer']) -dmxServerZmq = ServiceAddress(L9['dmxServerZmq']) -collector = ServiceAddress(L9['collector']) -collectorZmq = ServiceAddress(L9['collectorZmq']) -effectEval = ServiceAddress(L9['effectEval']) -effectSequencer = ServiceAddress(L9['effectSequencer']) -keyboardComposer = ServiceAddress(L9['keyboardComposer']) -musicPlayer = ServiceAddress(L9['musicPlayer']) -oscDmxServer = ServiceAddress(L9['oscDmxServer']) -paintServer = ServiceAddress(L9['paintServer']) -picamserve = ServiceAddress(L9['picamserve']) -rdfdb = ServiceAddress(L9['rdfdb']) -subComposer = ServiceAddress(L9['subComposer']) -subServer = ServiceAddress(L9['subServer']) -vidref = ServiceAddress(L9['vidref']) -timeline = ServiceAddress(L9['timeline']) - -patchReceiverUpdateHost = ServiceAddress(L9['patchReceiverUpdateHost']) diff -r 623836db99af -r 4556eebe5d73 light9/newtypes.py --- a/light9/newtypes.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,43 +0,0 @@ -from typing import NewType, Tuple, TypeVar, Union - -from rdflib import Literal, URIRef - -ClientType = NewType('ClientType', str) -ClientSessionType = NewType('ClientSessionType', str) -Curve = NewType('Curve', URIRef) -OutputUri = NewType('OutputUri', URIRef) # e.g. dmxA -DeviceUri = NewType('DeviceUri', URIRef) # e.g. :aura2 -DeviceClass = NewType('DeviceClass', URIRef) # e.g. :Aura -DmxIndex = NewType('DmxIndex', int) # 1..512 -DmxMessageIndex = NewType('DmxMessageIndex', int) # 0..511 -DeviceAttr = NewType('DeviceAttr', URIRef) # e.g. :rx -EffectFunction = NewType('EffectFunction', URIRef) # e.g. func:strobe -EffectUri = NewType('EffectUri', URIRef) # unclear when to use this vs EffectClass -EffectAttr = NewType('EffectAttr', URIRef) # e.g. :chaseSpeed -NoteUri = NewType('NoteUri', URIRef) -OutputAttr = NewType('OutputAttr', URIRef) # e.g. :xFine -OutputValue = NewType('OutputValue', int) # byte in dmx message -Song = NewType('Song', URIRef) -UnixTime = NewType('UnixTime', float) - -VT = TypeVar('VT', float, int, str) # remove -HexColor = NewType('HexColor', str) -VTUnion = Union[float, int, HexColor] # rename to ValueType -DeviceSetting = Tuple[DeviceUri, DeviceAttr, - # currently, floats and hex color strings - VTUnion] - -# Alternate output range for a device. Instead of outputting 0.0 to -# 1.0, you can map that range into, say, 0.2 to 0.7 -OutputRange = NewType('OutputRange', Tuple[float, float]) - - -def uriTail(u: URIRef) -> str: - tail = u.rstrip('/').rsplit('/', 1)[1] - if not tail: - tail = str(u) - return tail - - -def decimalLiteral(value): - return Literal(value, datatype='http://www.w3.org/2001/XMLSchema#decimal') diff -r 623836db99af -r 4556eebe5d73 light9/observable.py --- a/light9/observable.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,38 +0,0 @@ -import logging -log = logging.getLogger('observable') - - -class _NoNewVal: - pass - - -class Observable: - """ - like knockout's observable. Hopefully this can be replaced by a - better python one - - compare with: - http://knockoutjs.com/documentation/observables.html - https://github.com/drpancake/python-observable/blob/master/observable/observable.py - """ - - def __init__(self, val): - self.val = val - self.subscribers = set() - - def __call__(self, newVal=_NoNewVal): - if newVal is _NoNewVal: - return self.val - if newVal == self.val: - log.debug("%r unchanged from %r", newVal, self.val) - return - self.val = newVal - for s in self.subscribers: - s(newVal) - - def subscribe(self, cb, callNow=True): - """cb is called with new values, and also right now with the - current value unless you opt out""" - self.subscribers.add(cb) - if callNow: - cb(self.val) diff -r 623836db99af -r 4556eebe5d73 light9/paint/__init__.py diff -r 623836db99af -r 4556eebe5d73 light9/paint/capture.py --- a/light9/paint/capture.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,50 +0,0 @@ -import os -from rdflib import URIRef -from light9 import showconfig -from rdfdb.patch import Patch -from light9.namespaces import L9, RDF -from light9.paint.solve import loadNumpy - - -def writeCaptureDescription(graph, ctx, session, uri, dev, outPath, - settingsSubgraphCache, settings): - graph.patch( - Patch(addQuads=settings.statements( - uri, - ctx=ctx, - settingRoot=URIRef('/'.join( - [showconfig.showUri(), 'capture', - dev.rsplit('/')[1]])), - settingsSubgraphCache=settingsSubgraphCache))) - graph.patch( - Patch(addQuads=[ - (dev, L9['capture'], uri, ctx), - (session, L9['capture'], uri, ctx), - (uri, RDF.type, L9['LightSample'], ctx), - (uri, L9['imagePath'], - URIRef('/'.join([showconfig.showUri(), outPath])), ctx), - ])) - graph.suggestPrefixes( - ctx, { - 'cap': uri.rsplit('/', 1)[0] + '/', - 'showcap': showconfig.showUri() + '/capture/' - }) - - -class CaptureLoader(object): - - def __init__(self, graph): - self.graph = graph - - def loadImage(self, pic, thumb=(100, 100)): - ip = self.graph.value(pic, L9['imagePath']) - if not ip.startswith(showconfig.show()): - raise ValueError(repr(ip)) - diskPath = os.path.join(showconfig.root(), ip[len(self.show):]) - return loadNumpy(diskPath, thumb) - - def devices(self): - """devices for which we have any captured data""" - - def capturedSettings(self, device): - """list of (pic, settings) we know for this device""" diff -r 623836db99af -r 4556eebe5d73 light9/paint/solve.py --- a/light9/paint/solve.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,307 +0,0 @@ -from typing import List - -from rdflib import URIRef -import imageio -from light9.namespaces import L9, DEV -from PIL import Image -import numpy -import scipy.misc, scipy.ndimage, scipy.optimize -import cairo -import logging - -from light9.effect.settings import DeviceSettings, parseHex - -log = logging.getLogger('solve') - -# numpy images in this file are (x, y, c) layout. - - -def numpyFromCairo(surface): - w, h = surface.get_width(), surface.get_height() - a = numpy.frombuffer(surface.get_data(), numpy.uint8) - a.shape = h, w, 4 - a = a.transpose((1, 0, 2)) - return a[:w, :h, :3] - - -def numpyFromPil(img: Image.Image): - return numpy.asarray(img).transpose((1, 0, 2)) - - -def loadNumpy(path, thumb=(100, 100)): - img = Image.open(path) - img.thumbnail(thumb) - return numpyFromPil(img) - - -def saveNumpy(path, img): - # maybe this should only run if log level is debug? - imageio.imwrite(path, img.transpose((1, 0, 2))) - - -def scaledHex(h, scale): - rgb = parseHex(h) - rgb8 = (rgb * scale).astype(numpy.uint8) - return '#%02x%02x%02x' % tuple(rgb8) - - -def colorRatio(col1, col2): - rgb1 = parseHex(col1) - rgb2 = parseHex(col2) - - def div(x, y): - if y == 0: - return 0 - return round(x / y, 3) - - return tuple([div(a, b) for a, b in zip(rgb1, rgb2)]) - - -def brightest(img): - return numpy.amax(img, axis=(0, 1)) - - -class ImageDist(object): - - def __init__(self, img1): - self.a = img1.reshape((-1,)) - self.d = 255 * 255 * self.a.shape[0] - - def distanceTo(self, img2): - b = img2.reshape((-1,)) - return 1 - numpy.dot(self.a, b) / self.d - - -class ImageDistAbs(object): - - def __init__(self, img1): - self.a = img1 - self.maxDist = img1.shape[0] * img1.shape[1] * img1.shape[2] * 255 - - def distanceTo(self, img2): - return numpy.sum(numpy.absolute(self.a - img2), axis=None) / self.maxDist - - -class Solver(object): - - def __init__(self, graph, sessions:List[URIRef]|None=None, imgSize=(100, 53)): - self.graph = graph - self.sessions = sessions # URIs of capture sessions to load - self.imgSize = imgSize - self.samples = {} # uri: Image array (float 0-255) - self.fromPath = {} # imagePath: image array - self.path = {} # sample: path - self.blurredSamples = {} - self.sampleSettings = {} # sample: DeviceSettings - self.samplesForDevice = {} # dev : [(sample, img)] - - def loadSamples(self): - """learn what lights do from images""" - - log.info('loading...') - - with self.graph.currentState() as g: - for sess in self.sessions or []: - for cap in g.objects(sess, L9['capture']): - self._loadSample(g, cap) - log.info('loaded %s samples', len(self.samples)) - - def _loadSample(self, g, samp): - pathUri = g.value(samp, L9['imagePath']) - img = loadNumpy(pathUri.replace(L9[''], '')).astype(float) - settings = DeviceSettings.fromResource(self.graph, samp) - - self.samples[samp] = img - self.fromPath[pathUri] = img - self.blurredSamples[samp] = self._blur(img) - - self.path[samp] = pathUri - assert samp not in self.sampleSettings - self.sampleSettings[samp] = settings - devs = settings.devices() - if len(devs) == 1: - self.samplesForDevice.setdefault(devs[0], []).append((samp, img)) - - def _blur(self, img): - return scipy.ndimage.gaussian_filter(img, 10, 0, mode='nearest') - - def draw(self, painting): - return self._draw(painting, self.imgSize[0], self.imgSize[1]) - - def _draw(self, painting, w, h): - surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h) - ctx = cairo.Context(surface) - ctx.rectangle(0, 0, w, h) - ctx.fill() - - ctx.set_line_cap(cairo.LINE_CAP_ROUND) - ctx.set_line_width(w / 15) # ? - for stroke in painting['strokes']: - for pt in stroke['pts']: - op = ctx.move_to if pt is stroke['pts'][0] else ctx.line_to - op(pt[0] * w, pt[1] * h) - - r, g, b = parseHex(stroke['color']) - ctx.set_source_rgb(r / 255, g / 255, b / 255) - ctx.stroke() - - #surface.write_to_png('/tmp/surf.png') - return numpyFromCairo(surface) - - def bestMatch(self, img, device=None): - """the one sample that best matches this image""" - #img = self._blur(img) - results = [] - dist = ImageDist(img) - if device is None: - items = self.samples.items() - else: - items = self.samplesForDevice[device] - for uri, img2 in sorted(items): - if img.shape != img2.shape: - log.warn("mismatch %s %s", img.shape, img2.shape) - continue - results.append((dist.distanceTo(img2), uri, img2)) - results.sort() - topDist, topUri, topImg = results[0] - print('tops2') - for row in results[:4]: - print('%.5f' % row[0], row[1][-20:], self.sampleSettings[row[1]]) - - #saveNumpy('/tmp/best_in.png', img) - #saveNumpy('/tmp/best_out.png', topImg) - #saveNumpy('/tmp/mult.png', topImg / 255 * img) - return topUri, topDist - - def bestMatches(self, img, devices:List[URIRef]|None=None): - """settings for the given devices that point them each - at the input image""" - dist = ImageDist(img) - devSettings = [] - for dev in devices or []: - results = [] - for samp, img2 in self.samplesForDevice[dev]: - results.append((dist.distanceTo(img2), samp)) - results.sort() - - s = self.blendResults([(d, self.sampleSettings[samp]) for d, samp in results[:8]]) - devSettings.append(s) - return DeviceSettings.fromList(self.graph, devSettings) - - def blendResults(self, results): - """list of (dist, settings)""" - - dists = [d for d, sets in results] - hi = max(dists) - lo = min(dists) - n = len(results) - remappedDists = [1 - (d - lo) / (hi - lo) * n / (n + 1) for d in dists] - total = sum(remappedDists) - - #print 'blend' - #for o,n in zip(dists, remappedDists): - # print o,n, n / total - blend = DeviceSettings.fromBlend(self.graph, [(d / total, sets) for d, (_, sets) in zip(remappedDists, results)]) - return blend - - def solve(self, painting): - """ - given strokes of colors on a photo of the stage, figure out the - best light DeviceSettings to match the image - """ - pic0 = self.draw(painting).astype(numpy.float64) - pic0Blur = self._blur(pic0) - saveNumpy('/tmp/sample_paint_%s.png' % len(painting['strokes']), pic0Blur) - sampleDist = {} - dist = ImageDist(pic0Blur) - for sample, picSample in sorted(self.blurredSamples.items()): - #saveNumpy('/tmp/sample_%s.png' % sample.split('/')[-1], - # f(picSample)) - sampleDist[sample] = dist.distanceTo(picSample) - results = sorted([(d, uri) for uri, d in sampleDist.items()]) - - sample = results[0][1] - - # this is wrong; some wrong-alignments ought to be dimmer than full - brightest0 = brightest(pic0) - #brightestSample = brightest(self.samples[sample]) - - if max(brightest0) < 1 / 255: - return DeviceSettings(self.graph, []) - - #scale = brightest0 / brightestSample - - s = DeviceSettings.fromResource(self.graph, sample) - # missing color scale, but it was wrong to operate on all devs at once - return s - - def solveBrute(self, painting): - pic0 = self.draw(painting).astype(numpy.float64) - - colorSteps = 2 - colorStep = 1. / colorSteps - - # use toVector then add ranges - dims = [ - (DEV['aura1'], L9['rx'], [slice(.2, .7 + .1, .2)]), - (DEV['aura1'], L9['ry'], [slice(.573, .573 + 1, 1)]), - (DEV['aura1'], L9['color'], [slice(0, 1 + colorStep, colorStep), - slice(0, 1 + colorStep, colorStep), - slice(0, 1 + colorStep, colorStep)]), - ] - deviceAttrFilter = [(d, a) for d, a, s in dims] - - dist = ImageDist(pic0) - - def drawError(x): - settings = DeviceSettings.fromVector(self.graph, x, deviceAttrFilter=deviceAttrFilter) - preview = self.combineImages(self.simulationLayers(settings)) - #saveNumpy('/tmp/x_%s.png' % abs(hash(settings)), preview) - - out = dist.distanceTo(preview) - - #print 'measure at', x, 'drawError=', out - return out - - x0, fval, grid, Jout = scipy.optimize.brute(func=drawError, ranges=sum([s for dev, da, s in dims], []), finish=None, disp=True, full_output=True) - if fval > 30000: - raise ValueError('solution has error of %s' % fval) - return DeviceSettings.fromVector(self.graph, x0, deviceAttrFilter=deviceAttrFilter) - - def combineImages(self, layers): - """make a result image from our self.samples images""" - out = (next(iter(self.fromPath.values())) * 0).astype(numpy.uint16) - for layer in layers: - colorScaled = self.fromPath[layer['path']] * layer['color'] - out += colorScaled.astype(numpy.uint16) - numpy.clip(out, 0, 255, out) - return out.astype(numpy.uint8) - - def simulationLayers(self, settings): - """ - how should a simulation preview approximate the light settings - (device attribute values) by combining photos we have? - """ - assert isinstance(settings, DeviceSettings) - layers = [] - - for dev, devSettings in settings.byDevice(): - requestedColor = devSettings.getValue(dev, L9['color']) - candidatePics = [] # (distance, path, picColor) - for sample, s in self.sampleSettings.items(): - path = self.path[sample] - otherDevSettings = s.ofDevice(dev) - if not otherDevSettings: - continue - dist = devSettings.distanceTo(otherDevSettings) - log.info(' candidate pic %s %s dist=%s', sample, path, dist) - candidatePics.append((dist, path, s.getValue(dev, L9['color']))) - candidatePics.sort() - # we could even blend multiple top candidates, or omit all - # of them if they're too far - bestDist, bestPath, bestPicColor = candidatePics[0] - log.info(' device best d=%g path=%s color=%s', bestDist, bestPath, bestPicColor) - - layers.append({'path': bestPath, 'color': colorRatio(requestedColor, bestPicColor)}) - - return layers diff -r 623836db99af -r 4556eebe5d73 light9/paint/solve_test.py --- a/light9/paint/solve_test.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,157 +0,0 @@ -import unittest -import numpy.testing -from . import solve -from rdflib import Namespace -from light9.namespaces import L9, DEV -from light9.localsyncedgraph import LocalSyncedGraph -from light9.effect.settings import DeviceSettings - - -class TestSolve(unittest.TestCase): - - def setUp(self): - self.graph = LocalSyncedGraph( - files=['test/cam/lightConfig.n3', 'test/cam/bg.n3']) - self.solver = solve.Solver(self.graph, - imgSize=(100, 48), - sessions=[L9['session0']]) - self.solver.loadSamples() - self.solveMethod = self.solver.solve - - @unittest.skip('solveBrute unfinished') - def testBlack(self): - devAttrs = self.solveMethod({'strokes': []}) - self.assertEqual(DeviceSettings(self.graph, []), devAttrs) - - @unittest.skip("unfinished") - def testSingleLightCloseMatch(self): - devAttrs = self.solveMethod({ - 'strokes': [{ - 'pts': [[224, 141], [223, 159]], - 'color': '#ffffff' - }] - }) - self.assertEqual( - DeviceSettings(self.graph, [ - (DEV['aura1'], L9['color'], "#ffffff"), - (DEV['aura1'], L9['rx'], 0.5), - (DEV['aura1'], L9['ry'], 0.573), - ]), devAttrs) - - -class TestSolveBrute(TestSolve): - - def setUp(self): - super(TestSolveBrute, self).setUp() - self.solveMethod = self.solver.solveBrute - - -CAM_TEST = Namespace('http://light9.bigasterisk.com/test/cam/') - - -class TestSimulationLayers(unittest.TestCase): - - def setUp(self): - self.graph = LocalSyncedGraph( - files=['test/cam/lightConfig.n3', 'test/cam/bg.n3']) - self.solver = solve.Solver(self.graph, - imgSize=(100, 48), - sessions=[L9['session0']]) - self.solver.loadSamples() - - def testBlack(self): - self.assertEqual([], - self.solver.simulationLayers( - settings=DeviceSettings(self.graph, []))) - - def testPerfect1Match(self): - layers = self.solver.simulationLayers( - settings=DeviceSettings(self.graph, [( - DEV['aura1'], L9['color'], - "#ffffff"), (DEV['aura1'], L9['rx'], - 0.5), (DEV['aura1'], L9['ry'], 0.573)])) - self.assertEqual([{ - 'path': CAM_TEST['bg2-d.jpg'], - 'color': (1., 1., 1.) - }], layers) - - def testPerfect1MatchTinted(self): - layers = self.solver.simulationLayers( - settings=DeviceSettings(self.graph, [( - DEV['aura1'], L9['color'], - "#304050"), (DEV['aura1'], L9['rx'], - 0.5), (DEV['aura1'], L9['ry'], 0.573)])) - self.assertEqual([{ - 'path': CAM_TEST['bg2-d.jpg'], - 'color': (.188, .251, .314) - }], layers) - - def testPerfect2Matches(self): - layers = self.solver.simulationLayers( - settings=DeviceSettings(self.graph, [ - (DEV['aura1'], L9['color'], "#ffffff"), - (DEV['aura1'], L9['rx'], 0.5), - (DEV['aura1'], L9['ry'], 0.573), - (DEV['aura2'], L9['color'], "#ffffff"), - (DEV['aura2'], L9['rx'], 0.7), - (DEV['aura2'], L9['ry'], 0.573), - ])) - self.assertEqual([ - { - 'path': CAM_TEST['bg2-d.jpg'], - 'color': (1, 1, 1) - }, - { - 'path': CAM_TEST['bg2-f.jpg'], - 'color': (1, 1, 1) - }, - ], layers) - - -class TestCombineImages(unittest.TestCase): - - def setUp(self): - graph = LocalSyncedGraph( - files=['test/cam/lightConfig.n3', 'test/cam/bg.n3']) - self.solver = solve.Solver(graph, - imgSize=(100, 48), - sessions=[L9['session0']]) - self.solver.loadSamples() - - def fixme_test(self): - out = self.solver.combineImages(layers=[ - { - 'path': CAM_TEST['bg2-d.jpg'], - 'color': (.2, .2, .3) - }, - { - 'path': CAM_TEST['bg2-a.jpg'], - 'color': (.888, 0, .3) - }, - ]) - solve.saveNumpy('/tmp/t.png', out) - golden = solve.loadNumpy('test/cam/layers_out1.png') - numpy.testing.assert_array_equal(golden, out) - - -class TestBestMatch(unittest.TestCase): - - def setUp(self): - graph = LocalSyncedGraph( - files=['test/cam/lightConfig.n3', 'test/cam/bg.n3']) - self.solver = solve.Solver(graph, - imgSize=(100, 48), - sessions=[L9['session0']]) - self.solver.loadSamples() - - def testRightSide(self): - drawingOnRight = { - "strokes": [{ - "pts": [[0.875, 0.64], [0.854, 0.644]], - "color": "#aaaaaa" - }] - } - drawImg = self.solver.draw(drawingOnRight) - match, dist = self.solver.bestMatch(drawImg) - self.assertEqual(L9['sample5'], match) - self.assertAlmostEqual(0.983855965, dist, places=1) diff -r 623836db99af -r 4556eebe5d73 light9/prof.py --- a/light9/prof.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,65 +0,0 @@ -import sys, traceback, time, logging -from typing import Any, Dict -log = logging.getLogger() - - -def run(main, profile=None): - if not profile: - main() - return - - if profile == 'hotshot': - import hotshot, hotshot.stats - p = hotshot.Profile("/tmp/pro") - p.runcall(main) - p.close() - hotshot.stats.load("/tmp/pro").sort_stats('cumulative').print_stats() - elif profile == 'stat': - import statprof - statprof.start() - try: - main() - finally: - statprof.stop() - statprof.display() - - -def watchPoint(filename, lineno, event="call"): - """whenever we hit this line, print a stack trace. event='call' - for lines that are function definitions, like what a profiler - gives you. - - Switch to 'line' to match lines inside functions. Execution speed - will be much slower.""" - seenTraces: Dict[Any, int] = {} # trace contents : count - - def trace(frame, ev, arg): - if ev == event: - if (frame.f_code.co_filename, frame.f_lineno) == (filename, lineno): - stack = ''.join(traceback.format_stack(frame)) - if stack not in seenTraces: - print("watchPoint hit") - print(stack) - seenTraces[stack] = 1 - else: - seenTraces[stack] += 1 - - return trace - - sys.settrace(trace) - - # atexit, print the frequencies? - - -def logTime(func): - - def inner(*args, **kw): - t1 = time.time() - try: - ret = func(*args, **kw) - finally: - log.info("Call to %s took %.1f ms" % (func.__name__, 1000 * - (time.time() - t1))) - return ret - - return inner diff -r 623836db99af -r 4556eebe5d73 light9/rdfdb/service.py --- a/light9/rdfdb/service.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,28 +0,0 @@ -import logging -import os -from pathlib import Path - -from light9.run_local import log - -import rdfdb.service -from rdflib import URIRef - -from light9 import showconfig -logging.getLogger('rdfdb').setLevel(logging.INFO) -logging.getLogger('rdfdb.file').setLevel(logging.INFO) -logging.getLogger('rdfdb.graph').setLevel(logging.INFO) -logging.getLogger('rdfdb.net').setLevel(logging.INFO) -rdfRoot = Path(os.environ['LIGHT9_SHOW'].rstrip('/') + '/') -showUri = URIRef(showconfig.showUri() + '/') - -app = rdfdb.service.makeApp( # - dirUriMap={rdfRoot: showUri}, - prefixes={ - 'show': showUri, - '': URIRef('http://light9.bigasterisk.com/'), - 'rdf': URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#'), - 'rdfs': URIRef('http://www.w3.org/2000/01/rdf-schema#'), - 'xsd': URIRef('http://www.w3.org/2001/XMLSchema#'), - 'effect': URIRef('http://light9.bigasterisk.com/effect/'), - 'dev': URIRef('http://light9.bigasterisk.com/theater/skyline/device/'), - }) diff -r 623836db99af -r 4556eebe5d73 light9/rdfdb/service_test.py --- a/light9/rdfdb/service_test.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,11 +0,0 @@ -import asyncio -from light9.run_local import log - - -def test_import(): - - async def go(): - # this sets up some watcher tasks - from light9.rdfdb.service import app - - asyncio.run(go(), debug=True) diff -r 623836db99af -r 4556eebe5d73 light9/recentfps.py --- a/light9/recentfps.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,30 +0,0 @@ -# server side version of what the metrics consumer does with changing counts -import time - -class RecentFps: - def __init__(self, window=20): - self.window = window - self.recentTimes = [] - - def mark(self): - now = time.time() - self.recentTimes.append(now) - self.recentTimes = self.recentTimes[-self.window:] - - def rate(self): - def dec(innerFunc): - def f(*a, **kw): - self.mark() - return innerFunc(*a, **kw) - return f - return dec - - def __call__(self): - if len(self.recentTimes) < 2: - return {} - recents = sorted(round(1 / (b - a), 3) - for a, b in zip(self.recentTimes[:-1], - self.recentTimes[1:])) - avg = (len(self.recentTimes) - 1) / ( - self.recentTimes[-1] - self.recentTimes[0]) - return {'average': round(avg, 5), 'recents': recents} diff -r 623836db99af -r 4556eebe5d73 light9/run_local.py --- a/light9/run_local.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,63 +0,0 @@ -# bootstrap - -import re -import logging -import os -import socket -import sys -from colorsys import hsv_to_rgb - -import coloredlogs - -try: - import faulthandler - faulthandler.enable() -except ImportError: - pass - -if 0: - from IPython.core import ultratb - sys.excepthook = ultratb.FormattedTB(mode='Verbose', color_scheme='Linux', call_pdb=1) - -progName = sys.argv[0].split('/')[-1] -log = logging.getLogger() # this has to get the root logger -log.name = progName # but we can rename it for clarity - -coloredlogs.install( - level='DEBUG', - milliseconds=True, - datefmt='t=%H:%M:%S', - fmt='%(asctime)s.%(msecs)03d %(levelname)1.1s [%(filename)s:%(lineno)s] %(name)s %(message)s', - # try `pdm run humanfriendly --demo` - field_styles=dict( - asctime=dict(color=30), - levelname=dict(color=247), - name=dict(color='blue'), - ), - level_styles={ - 'debug': dict(color=115), - 'info': dict(color=250), - 'warning': dict(color=208), - 'error': dict(color=161), - 'critical': dict(color=196), - }, -) - - -def setTerminalTitle(s): - if os.environ.get('TERM', '') in ['xterm', 'rxvt', 'rxvt-unicode-256color']: - m = re.search(r'(light9\..*):', s) - if m is not None: - s = m.group(1) - s = s.replace('/home/drewp/own/proj/','') - print("\033]0;%s\007" % s) # not escaped/protected correctly - hue = (hash(progName) % 255) / 255 - r, g, b = [int(x * 255) for x in hsv_to_rgb(hue, s=.2, v=.1)] - print(f"\033]11;#{r:02x}{g:02x}{b:02x}\007") - - -if 'listsongs' not in sys.argv[0] and 'homepageConfig' not in sys.argv[0]: - setTerminalTitle('[%s] %s' % (socket.gethostname(), ' '.join(sys.argv).replace('bin/', ''))) - -# see http://www.youtube.com/watch?v=3cIOT9kM--g for commands that make -# profiles and set background images diff -r 623836db99af -r 4556eebe5d73 light9/showconfig.py --- a/light9/showconfig.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,97 +0,0 @@ -import logging, warnings -from twisted.python.filepath import FilePath -from os import path, getenv -from rdflib import Graph -from rdflib import URIRef, Literal -from .namespaces import L9 -from typing import List, cast -log = logging.getLogger('showconfig') - -_config = None # graph - - -def getGraph() -> Graph: - warnings.warn( - "code that's using showconfig.getGraph should be " - "converted to use the sync graph", - stacklevel=2) - global _config - if _config is None: - graph = Graph() - # note that logging is probably not configured the first time - # we're in here - warnings.warn("reading n3 files around %r" % root()) - for f in FilePath(root()).globChildren("*.n3") + FilePath( - root()).globChildren("build/*.n3"): - graph.parse(location=f.path, format='n3') - _config = graph - return _config - - -def root() -> bytes: - r = getenv("LIGHT9_SHOW") - if r is None: - raise OSError( - "LIGHT9_SHOW env variable has not been set to the show root") - return r.encode('ascii') - - -_showUri = None - - -def showUri() -> URIRef: - """Return the show URI associated with $LIGHT9_SHOW.""" - global _showUri - if _showUri is None: - _showUri = URIRef(open(path.join(root(), b'URI')).read().strip()) - return _showUri - - -def songOnDisk(song: URIRef) -> bytes: - """given a song URI, where's the on-disk file that mpd would read?""" - graph = getGraph() - root = graph.value(showUri(), L9['musicRoot']) - if not root: - raise ValueError("%s has no :musicRoot" % showUri()) - - name = graph.value(song, L9['songFilename']) - if not name: - raise ValueError("Song %r has no :songFilename" % song) - - return path.abspath( - path.join( - cast(Literal, root).toPython(), - cast(Literal, name).toPython())) - - -def songFilenameFromURI(uri: URIRef) -> bytes: - """ - 'http://light9.bigasterisk.com/show/dance2007/song8' -> 'song8' - - everything that uses this should be deprecated for real URIs - everywhere""" - assert isinstance(uri, URIRef) - return str(uri).split('/')[-1].encode('ascii') - - -def getSongsFromShow(graph: Graph, show: URIRef) -> List[URIRef]: - playList = graph.value(show, L9['playList']) - if not playList: - raise ValueError("%r has no l9:playList" % show) - # The patch in https://github.com/RDFLib/rdflib/issues/305 fixed a - # serious bug here. - songs = list(graph.items(playList)) - - return songs - - -def curvesDir(): - return path.join(root(), b"curves") - - -def subFile(subname): - return path.join(root(), b"subs", subname) - - -def subsDir(): - return path.join(root(), b'subs') diff -r 623836db99af -r 4556eebe5d73 light9/subclient.py --- a/light9/subclient.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,55 +0,0 @@ -from light9.collector.collector_client import sendToCollector -from twisted.internet import reactor -from twisted.internet.defer import Deferred -import traceback -import time -import logging -from rdflib import URIRef -from rdfdb.syncedgraph import SyncedGraph -log = logging.getLogger() - - -class SubClient: - graph: SyncedGraph - session: URIRef - - def __init__(self): - """assumed that your init saves self.graph""" - pass # we may later need init code for network setup - - def get_levels_as_sub(self): - """Subclasses must implement this method and return a Submaster - object.""" - - def send_levels_loop(self, periodSec=1.) -> None: - sendStartTime = time.time() - - def done(sec): - delay = max(0, (sendStartTime + periodSec) - time.time()) - reactor.callLater(delay, self.send_levels_loop, periodSec) - - def err(e): - log.warn('subclient loop: %r', e) - reactor.callLater(2, self.send_levels_loop, periodSec) - - d = self._send_sub() - d.addCallbacks(done, err) - - def _send_sub(self) -> Deferred: - try: - with self.graph.currentState() as g: - outputSettings = self.get_output_settings(_graph=g) - except Exception: - traceback.print_exc() - raise - - return sendToCollector( - 'subclient', - self.session, - outputSettings, - # when KC uses zmq, we get message - # pileups and delays on collector (even - # at 20fps). When sequencer uses zmp, - # it runs great at 40fps. Not sure the - # difference- maybe Tk main loop? - useZmq=False) diff -r 623836db99af -r 4556eebe5d73 light9/subcomposer/__init__.py diff -r 623836db99af -r 4556eebe5d73 light9/subcomposer/index.html --- a/light9/subcomposer/index.html Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,65 +0,0 @@ - - - - subcomposer - - - - -
Toggle channel in current sub
- - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - diff -r 623836db99af -r 4556eebe5d73 light9/subcomposer/subcomposerweb.py --- a/light9/subcomposer/subcomposerweb.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,38 +0,0 @@ -import logging - -from rdflib import URIRef, Literal -from twisted.internet import reactor -import cyclone.web - -from cycloneerr import PrettyErrorHandler -from light9 import networking - -log = logging.getLogger('web') - - -def init(graph, session, currentSub): - SFH = cyclone.web.StaticFileHandler - app = cyclone.web.Application(handlers=[ - (r'/()', SFH, { - 'path': 'light9/subcomposer', - 'default_filename': 'index.html' - }), - (r'/toggle', Toggle), - ], - debug=True, - graph=graph, - currentSub=currentSub) - reactor.listenTCP(networking.subComposer.port, app) - log.info("listening on %s" % networking.subComposer.port) - - -class Toggle(PrettyErrorHandler, cyclone.web.RequestHandler): - - def post(self): - chan = URIRef(self.get_argument('chan')) - sub = self.settings.currentSub() - - chanKey = Literal(chan.rsplit('/', 1)[1]) - old = sub.get_levels().get(chanKey, 0) - - sub.editLevel(chan, 0 if old else 1) diff -r 623836db99af -r 4556eebe5d73 light9/tkdnd.py --- a/light9/tkdnd.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,156 +0,0 @@ -from glob import glob -from os.path import join, basename -from typing import Dict, Any - - -class TkdndEvent(object): - """ - see http://www.ellogon.org/petasis/tcltk-projects/tkdnd/tkdnd-man-page - for details on the fields - - The longer attribute names (action instead of %A) were made up for - this API. - - Not all attributes are visible yet, since I have not thought - through what conversions they should receive and I don't want to - unnecessarily change their types later. - """ - substitutions = { - "%A": "action", - "%b": "button", - "%D": "data", - "%m": "modifiers", - "%T": "type", - "%W": "targetWindow", - "%X": "mouseX", - "%Y": "mouseY", - } - - @classmethod - def makeEvent(cls, *args): - ev = cls() - for (k, v), arg in zip(sorted(TkdndEvent.substitutions.items()), args): - setattr(ev, v, arg) - # it would be cool for this to decode text data according to the charset in the type - for attr in ['button', 'mouseX', 'mouseY']: - setattr(ev, attr, int(getattr(ev, attr))) - return (ev,) - - tclSubstitutions = ' '.join(sorted(substitutions.keys())) - - def __repr__(self): - return "" % self.__dict__ - - -class Hover(object): - - def __init__(self, widget, style): - self.widget, self.style = widget, style - self.oldStyle: Dict[Any, Any] = {} - - def set(self, ev): - for k, v in list(self.style.items()): - self.oldStyle[k] = self.widget.cget(k) - self.widget.configure(**self.style) - return ev.action - - def restore(self, ev): - self.widget.configure(**self.oldStyle) - - -def initTkdnd(tk, tkdndBuildDir): - """ - pass the 'tk' attribute of any Tkinter object, and the top dir of - your built tkdnd package - """ - tk.call('source', join(tkdndBuildDir, 'library/tkdnd.tcl')) - for dll in glob( - join(tkdndBuildDir, - '*tkdnd*' + tk.call('info', 'sharedlibextension'))): - tk.call('tkdnd::initialise', join(tkdndBuildDir, 'library'), - join('..', basename(dll)), 'tkdnd') - - -def dragSourceRegister(widget, action='copy', datatype='text/uri-list', - data=''): - """ - if the 'data' param is callable, it will be called every time to - look up the current data. - - If the callable returns None (or data is None to begin with), the drag - """ - widget.tk.call('tkdnd::drag_source', 'register', widget._w) - - # with normal Tkinter bind(), the result of your handler isn't - # actually returned so the drag doesn't get launched. This is a - # corrected version of what bind() does when you pass a function, - # but I don't block my tuple from getting returned (as a tcl list) - - def init(): - dataValue = data() if callable(data) else data - if dataValue is None: - return - return (action, datatype, dataValue) - - funcId = widget._register( - init, - widget._substitute, - 1 # needscleanup - ) - widget.bind("<>", funcId) - - -def dropTargetRegister( - widget, - typeList=None, - onDropEnter=None, - onDropPosition=None, - onDropLeave=None, - onDrop=None, - hoverStyle=None, -): - """ - the optional callbacks will be called with a TkdndEvent - argument. - - onDropEnter, onDropPosition, and onDrop are supposed to return an - action (perhaps the value in event.action). The return value seems - to have no effect, but it might just be that errors are getting - silenced. - - Passing hoverStyle sets onDropEnter to call - widget.configure(**hoverStyle) and onDropLeave to restore the - widget's style. onDrop is also wrapped to do a restore. - """ - - if hoverStyle is not None: - hover = Hover(widget, hoverStyle) - - def wrappedDrop(ev): - hover.restore(ev) - if onDrop: - return onDrop(ev) - - return dropTargetRegister(widget, - typeList=typeList, - onDropEnter=hover.set, - onDropLeave=hover.restore, - onDropPosition=onDropPosition, - onDrop=wrappedDrop) - - if typeList is None: - typeList = ['*'] - widget.tk.call(*(['tkdnd::drop_target', 'register', widget._w] + typeList)) - - for sequence, handler in [ - ('<>', onDropEnter), - ('<>', onDropPosition), - ('<>', onDropLeave), - ('<>', onDrop), - ]: - if not handler: - continue - func = widget._register(handler, - subst=TkdndEvent.makeEvent, - needcleanup=1) - widget.bind(sequence, func + " " + TkdndEvent.tclSubstitutions) diff -r 623836db99af -r 4556eebe5d73 light9/typedgraph.py --- a/light9/typedgraph.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,87 +0,0 @@ -from typing import List, Type, TypeVar, cast, get_args - -from rdfdb.syncedgraph.syncedgraph import SyncedGraph -from rdflib import XSD, BNode, Graph, Literal, URIRef -from rdflib.term import Node - -# todo: this ought to just require a suitable graph.value method -EitherGraph = Graph | SyncedGraph - -_ObjType = TypeVar('_ObjType') - - -class ConversionError(ValueError): - """graph had a value, but it does not safely convert to any of the requested types""" - - -def _expandUnion(t: Type) -> List[Type]: - if hasattr(t, '__args__'): - return list(get_args(t)) - return [t] - - -def _typeIncludes(t1: Type, t2: Type) -> bool: - """same as issubclass but t1 can be a NewType""" - if t2 is None: - t2 = type(None) - if t1 == t2: - return True - - if getattr(t1, '__supertype__', None) == t2: - return True - - ts = _expandUnion(t1) - if len(ts) > 1: - return any(_typeIncludes(t, t2) for t in ts) - - return False - - -def _convLiteral(objType: Type[_ObjType], x: Literal) -> _ObjType: - if _typeIncludes(objType, Literal): - return cast(objType, x) - - for outType, dtypes in [ - (float, (XSD['integer'], XSD['double'], XSD['decimal'])), - (int, (XSD['integer'],)), - (str, ()), - ]: - for t in _expandUnion(objType): - if _typeIncludes(t, outType) and (not dtypes or x.datatype in dtypes): - # e.g. user wants float and we have xsd:double - return cast(objType, outType(x.toPython())) - raise ConversionError - - -def typedValue(objType: Type[_ObjType], graph: EitherGraph, subj: Node, pred: URIRef) -> _ObjType: - """graph.value(subj, pred) with a given return type. - If objType is not an rdflib.Node, we toPython() the value. - - Allow objType to include None if you want a None return for not-found. - """ - if objType is None: - raise TypeError('must allow non-None result type') - obj = graph.value(subj, pred) - if obj is None: - if _typeIncludes(objType, None): - return cast(objType, None) - raise ValueError(f'No obj for {subj=} {pred=}') - - ConvFrom: Type[Node] = type(obj) - ConvTo = objType - try: - if ConvFrom == URIRef and _typeIncludes(ConvTo, URIRef): - conv = obj - elif ConvFrom == URIRef and issubclass(URIRef, ConvTo) and not issubclass(str, ConvTo): # rewrite please - conv = obj - elif ConvFrom == BNode and issubclass(BNode, ConvTo): - conv = obj - elif ConvFrom == Literal: - conv = _convLiteral(objType, cast(Literal, obj)) - else: - raise ConversionError - except ConversionError: - raise ConversionError(f'graph contains {type(obj)}, caller requesting {objType}') - # if objType is float and isinstance(conv, decimal.Decimal): - # conv = float(conv) - return cast(objType, conv) \ No newline at end of file diff -r 623836db99af -r 4556eebe5d73 light9/typedgraph_test.py --- a/light9/typedgraph_test.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,161 +0,0 @@ -from typing import NewType, Optional, cast - -import pytest -from rdflib import BNode, Graph, Literal, URIRef -from rdflib.term import Node -from light9.mock_syncedgraph import MockSyncedGraph -from light9.namespaces import L9, XSD -from light9.typedgraph import ConversionError, _typeIncludes, typedValue - -g = cast( - Graph, - MockSyncedGraph(''' - @prefix : . - :subj - :uri :c; - :bnode []; - # see https://w3c.github.io/N3/spec/#literals for syntaxes. - :int 0; - :float1 0.0; - :float2 1.0e1; - :float3 0.5; - :color "#ffffff"^^:hexColor; - :definitelyAString "hello" . -''')) - -subj = L9['subj'] - - -class TestTypeIncludes: - - def test_includesItself(self): - assert _typeIncludes(str, str) - - def test_includesUnionMember(self): - assert _typeIncludes(int | str, str) - - def test_notIncludes(self): - assert not _typeIncludes(int | str, None) - - def test_explicitOptionalWorks(self): - assert _typeIncludes(Optional[int], None) - - def test_3WayUnionWorks(self): - assert _typeIncludes(int | str | float, int) - - -class TestTypedValueReturnsBasicTypes: - - def test_getsUri(self): - assert typedValue(URIRef, g, subj, L9['uri']) == L9['c'] - - def test_getsAsNode(self): - assert typedValue(Node, g, subj, L9['uri']) == L9['c'] - - def test_getsBNode(self): - # this is unusual usage since users ought to always be able to replace BNode with URIRef - assert typedValue(BNode, g, subj, L9['bnode']) == g.value(subj, L9['bnode']) - - def test_getsBNodeAsNode(self): - assert typedValue(Node, g, subj, L9['bnode']) == g.value(subj, L9['bnode']) - - - def test_getsNumerics(self): - assert typedValue(float, g, subj, L9['int']) == 0 - assert typedValue(float, g, subj, L9['float1']) == 0 - assert typedValue(float, g, subj, L9['float2']) == 10 - assert typedValue(float, g, subj, L9['float3']) == 0.5 - - assert typedValue(int, g, subj, L9['int']) == 0 - # These retrieve rdf floats that happen to equal - # ints, but no one should be relying on that. - with pytest.raises(ConversionError): - typedValue(int, g, subj, L9['float1']) - with pytest.raises(ConversionError): - typedValue(int, g, subj, L9['float2']) - with pytest.raises(ConversionError): - typedValue(int, g, subj, L9['float3']) - - def test_getsString(self): - tv = typedValue(str, g, subj, L9['color']) - assert tv == '#ffffff' - - def test_getsLiteral(self): - tv = typedValue(Literal, g, subj, L9['float2']) - assert type(tv) == Literal - assert tv.datatype == XSD['double'] - - tv = typedValue(Literal, g, subj, L9['color']) - assert type(tv) == Literal - assert tv.datatype == L9['hexColor'] - - -class TestTypedValueDoesntDoInappropriateUriStringConversions: - - def test_noUriToString(self): - with pytest.raises(ConversionError): - typedValue(str, g, subj, L9['uri']) - - def test_noUriToLiteral(self): - with pytest.raises(ConversionError): - typedValue(Literal, g, subj, L9['uri']) - - def test_noStringToUri(self): - with pytest.raises(ConversionError): - typedValue(URIRef, g, subj, L9['definitelyAString']) - - -class TestTypedValueOnMissingValues: - - def test_errorsOnMissingValue(self): - with pytest.raises(ValueError): - typedValue(float, g, subj, L9['missing']) - - def test_returnsNoneForMissingValueIfCallerPermits(self): - assert (float | None) == Optional[float] - assert typedValue(float | None, g, subj, L9['float1']) == 0 - assert typedValue(float | None, g, subj, L9['missing']) == None - assert typedValue(str | float | None, g, subj, L9['missing']) == None - - def test_cantJustPassNone(self): - with pytest.raises(TypeError): - typedValue(None, g, subj, L9['float1']) # type: ignore - - -class TestTypedValueConvertsToNewTypes: - - def test_castsUri(self): - DeviceUri = NewType('DeviceUri', URIRef) - assert typedValue(DeviceUri, g, subj, L9['uri']) == DeviceUri(L9['c']) - - def test_castsLiteralToNewType(self): - HexColor = NewType('HexColor', str) - assert typedValue(HexColor, g, subj, L9['color']) == HexColor('#ffffff') - - -class TestTypedValueAcceptsUnionTypes: - - def test_getsMemberTypeOfUnion(self): - tv1 = typedValue(float | str, g, subj, L9['float1']) - assert type(tv1) == float - assert tv1 == 0.0 - - tv2 = typedValue(float | str, g, subj, L9['color']) - assert type(tv2) == str - assert tv2 == '#ffffff' - - def test_failsIfNoUnionTypeMatches(self): - with pytest.raises(ConversionError): - typedValue(float | URIRef, g, subj, L9['color']) - - def test_combinesWithNone(self): - assert typedValue(float | URIRef | None, g, subj, L9['uri']) == L9['c'] - - def test_combinedWithNewType(self): - HexColor = NewType('HexColor', str) - assert typedValue(float | HexColor, g, subj, L9['float1']) == 0 - assert typedValue(float | HexColor, g, subj, L9['color']) == HexColor('#ffffff') - - def test_whenOneIsUri(self): - assert typedValue(str | URIRef, g, subj, L9['color']) == '#ffffff' - assert typedValue(str | URIRef, g, subj, L9['uri']) == L9['c'] diff -r 623836db99af -r 4556eebe5d73 light9/uihelpers.py --- a/light9/uihelpers.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,298 +0,0 @@ -"""all the tiny tk helper functions""" - -#from Tkinter import Button -import logging, time -from rdflib import Literal -from tkinter.tix import Button, Toplevel, Tk, IntVar, Entry, DoubleVar -import tkinter -from light9.namespaces import L9 -from typing import Dict - -log = logging.getLogger("toplevel") - -windowlocations = { - 'sub': '425x738+00+00', - 'console': '168x24+848+000', - 'leveldisplay': '144x340+870+400', - 'cuefader': '314x212+546+741', - 'effect': '24x24+0963+338', - 'stage': '823x683+37+030', - 'scenes': '504x198+462+12', -} - - -def bindkeys(root, key, func): - root.bind(key, func) - for w in root.winfo_children(): - w.bind(key, func) - - -def toplevel_savegeometry(tl, name): - try: - geo = tl.geometry() - if not geo.startswith("1x1"): - f = open(".light9-window-geometry-%s" % name.replace(' ', '_'), 'w') - f.write(tl.geometry()) - # else the window never got mapped - except Exception: - # it's ok if there's no saved geometry - pass - - -def toplevelat(name, existingtoplevel=None, graph=None, session=None): - tl = existingtoplevel or Toplevel() - tl.title(name) - - lastSaved = [None] - setOnce = [False] - graphSetTime = [0.0] - - def setPosFromGraphOnce(): - """ - the graph is probably initially empty, but as soon as it gives - us one window position, we stop reading them - """ - if setOnce[0]: - return - geo = graph.value(session, L9['windowGeometry']) - log.debug("setPosFromGraphOnce %s", geo) - - setOnce[0] = True - graphSetTime[0] = time.time() - if geo is not None and geo != lastSaved[0]: - tl.geometry(geo) - lastSaved[0] = geo - - def savePos(ev): - geo = tl.geometry() - if not isinstance(ev.widget, (Tk, tkinter.Tk)): - # I think these are due to internal widget size changes, - # not the toplevel changing - return - # this is trying to not save all the startup automatic window - # sizes. I don't have a better plan for this yet. - if graphSetTime[0] == 0 or time.time() < graphSetTime[0] + 3: - return - if not setOnce[0]: - return - lastSaved[0] = geo - log.debug("saving position %s", geo) - graph.patchObject(session, session, L9['windowGeometry'], Literal(geo)) - - if graph is not None and session is not None: - graph.addHandler(setPosFromGraphOnce) - - if name in windowlocations: - tl.geometry(positionOnCurrentDesktop(windowlocations[name])) - - if graph is not None: - tl._toplevelat_funcid = tl.bind( - "", lambda ev, tl=tl, name=name: savePos(ev)) - - return tl - - -def positionOnCurrentDesktop(xform, screenWidth=1920, screenHeight=1440): - size, x, y = xform.split('+') - x = int(x) % screenWidth - y = int(y) % screenHeight - return "%s+%s+%s" % (size, x, y) - - -def toggle_slider(s): - if s.get() == 0: - s.set(100) - else: - s.set(0) - - -# for lambda callbacks -def printout(t): - print('printout', t) - - -def printevent(ev): - for k in dir(ev): - if not k.startswith('__'): - print('ev', k, getattr(ev, k)) - - -def eventtoparent(ev, sequence): - "passes an event to the parent, screws up TixComboBoxes" - - wid_class = str(ev.widget.__class__) - if wid_class == 'Tix.ComboBox' or wid_class == 'Tix.TixSubWidget': - return - - evdict = {} - for x in ['state', 'time', 'y', 'x', 'serial']: - evdict[x] = getattr(ev, x) - - -# evdict['button']=ev.num - par = ev.widget.winfo_parent() - if par != ".": - ev.widget.nametowidget(par).event_generate(sequence, **evdict) - #else the event made it all the way to the top, unhandled - - -def colorlabel(label): - """color a label based on its own text""" - txt = label['text'] or "0" - lev = float(txt) / 100 - low = (80, 80, 180) - high = (255, 55, 0o50) - out = [int(l + lev * (h - l)) for h, l in zip(high, low)] - col = "#%02X%02X%02X" % tuple(out) # type: ignore - label.config(bg=col) - - -# TODO: get everyone to use this -def colorfade(low, high, percent): - '''not foolproof. make sure 0 < percent < 1''' - out = [int(l + percent * (h - l)) for h, l in zip(high, low)] - col = "#%02X%02X%02X" % tuple(out) # type: ignore - return col - - -def colortotuple(anytkobj, colorname): - 'pass any tk object and a color name, like "yellow"' - rgb = anytkobj.winfo_rgb(colorname) - return [v / 256 for v in rgb] - - -class Togglebutton(Button): - """works like a single radiobutton, but it's a button so the - label's on the button face, not to the side. the optional command - callback is called on button set, not on unset. takes a variable - just like a checkbutton""" - - def __init__(self, - parent, - variable=None, - command=None, - downcolor='red', - **kw): - - self.oldcommand = command - Button.__init__(self, parent, command=self.invoke, **kw) - - self._origbkg = self.cget('bg') - self.downcolor = downcolor - - self._variable = variable - if self._variable: - self._variable.trace('w', self._varchanged) - self._setstate(self._variable.get()) - else: - self._setstate(0) - - self.bind("", self.invoke) - self.bind("<1>", self.invoke) - self.bind("", self.invoke) - - def _varchanged(self, *args): - self._setstate(self._variable.get()) - - def invoke(self, *ev): - if self._variable: - self._variable.set(not self.state) - else: - self._setstate(not self.state) - - if self.oldcommand and self.state: # call command only when state goes to 1 - self.oldcommand() - return "break" - - def _setstate(self, newstate): - self.state = newstate - if newstate: # set - self.config(bg=self.downcolor, relief='sunken') - else: # unset - self.config(bg=self._origbkg, relief='raised') - return "break" - - -class FancyDoubleVar(DoubleVar): - - def __init__(self, master=None): - DoubleVar.__init__(self, master) - self.callbacklist: Dict[str, str] = {} # cbname : mode - self.namedtraces: Dict[str, str] = {} # name : cbname - - def trace_variable(self, mode, callback): - """Define a trace callback for the variable. - - MODE is one of "r", "w", "u" for read, write, undefine. - CALLBACK must be a function which is called when - the variable is read, written or undefined. - - Return the name of the callback. - """ - cbname = self._master._register(callback) - self._tk.call("trace", "variable", self._name, mode, cbname) - - # we build a list of the trace callbacks (the py functrions and the tcl functionnames) - self.callbacklist[cbname] = mode - # print "added trace:",callback,cbname - - return cbname - - trace = trace_variable - - def disable_traces(self): - for cb, mode in list(self.callbacklist.items()): - # DoubleVar.trace_vdelete(self,v[0],k) - self._tk.call("trace", "vdelete", self._name, mode, cb) - # but no master delete! - - def recreate_traces(self): - for cb, mode in list(self.callbacklist.items()): - # self.trace_variable(v[0],v[1]) - self._tk.call("trace", "variable", self._name, mode, cb) - - def trace_named(self, name, callback): - if name in self.namedtraces: - print( - "FancyDoubleVar: already had a trace named %s - replacing it" % - name) - self.delete_named(name) - - cbname = self.trace_variable( - 'w', callback) # this will register in self.callbacklist too - - self.namedtraces[name] = cbname - return cbname - - def delete_named(self, name): - if name in self.namedtraces: - - cbname = self.namedtraces[name] - - self.trace_vdelete('w', cbname) - #self._tk.call("trace","vdelete",self._name,'w',cbname) - print("FancyDoubleVar: successfully deleted trace named %s" % name) - else: - print( - "FancyDoubleVar: attempted to delete named %s which wasn't set to any function" - % name) - - -def get_selection(listbox): - 'Given a listbox, returns first selection as integer' - selection = int(listbox.curselection()[0]) # blech - return selection - - -if __name__ == '__main__': - root = Tk() - root.tk_focusFollowsMouse() - iv = IntVar() - - def cb(): - print("cb!") - - t = Togglebutton(root, text="testbutton", command=cb, variable=iv) - t.pack() - Entry(root, textvariable=iv).pack() - root.mainloop() diff -r 623836db99af -r 4556eebe5d73 light9/updatefreq.py --- a/light9/updatefreq.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,33 +0,0 @@ -"""calculates your updates-per-second""" - -import time - - -class Updatefreq: - """make one of these, call update() on it as much as you want, - and then float() or str() the object to learn the updates per second. - - the samples param to __init__ specifies how many past updates will - be stored. """ - - def __init__(self, samples=20): - self.times = [0] - self.samples = samples - - def update(self): - """call this every time you do an update""" - self.times = self.times[-self.samples:] - self.times.append(time.time()) - - def __float__(self): - """a cheap algorithm, for now, which looks at the first and - last times only""" - - try: - hz = len(self.times) / (self.times[-1] - self.times[0]) - except ZeroDivisionError: - return 0.0 - return hz - - def __str__(self): - return "%.2fHz" % float(self) diff -r 623836db99af -r 4556eebe5d73 light9/vidref/__init__.py diff -r 623836db99af -r 4556eebe5d73 light9/vidref/gui.js --- a/light9/vidref/gui.js Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,11 +0,0 @@ -var model = { - shutters: [], -}; -for (var s=0; s < 1; s+=.04) { - var micro = Math.floor(Math.pow(s, 3) * 100000) - if (micro == 0) { - continue; - } - model.shutters.push(micro); -} -ko.applyBindings(model) diff -r 623836db99af -r 4556eebe5d73 light9/vidref/index.html --- a/light9/vidref/index.html Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,57 +0,0 @@ - - - - vidref - - - - - - - - - - - -

vidref

-
- -
- -
Keys: - s stop, - p play, - ,/. step -
- - - diff -r 623836db99af -r 4556eebe5d73 light9/vidref/main.py --- a/light9/vidref/main.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,110 +0,0 @@ -#!/usr/bin/python -""" - -dvcam test -gst-launch dv1394src ! dvdemux name=d ! dvdec ! ffmpegcolorspace ! hqdn3d ! xvimagesink - -""" -import gobject, logging, traceback -import gtk -from twisted.python.util import sibpath -from light9.vidref.replay import ReplayViews, framerate -from light9.ascoltami.musictime_client import MusicTime -from light9.vidref.videorecorder import Pipeline -from light9.vidref import remotepivideo -log = logging.getLogger() - - -class Gui(object): - - def __init__(self, graph): - wtree = gtk.Builder() - wtree.add_from_file(sibpath(__file__, "vidref.glade")) - mainwin = wtree.get_object("MainWindow") - mainwin.connect("destroy", gtk.main_quit) - wtree.connect_signals(self) - gtk.rc_parse("theme/marble-ice/gtk-2.0/gtkrc") - - self.recordingTo = wtree.get_object('recordingTo') - self.musicScale = wtree.get_object("musicScale") - self.musicScale.connect("value-changed", self.onMusicScaleValue) - # tiny race here if onMusicScaleValue tries to use musicTime right away - self.musicTime = MusicTime(onChange=self.onMusicTimeChange, - pollCurvecalc=False) - self.ignoreScaleChanges = False - # self.attachLog(wtree.get_object("lastLog")) # disabled due to crashing - - # wtree.get_object("replayPanel").show() # demo only - rp = wtree.get_object("replayVbox") - self.replayViews = ReplayViews(rp) - - mainwin.show_all() - vid3 = wtree.get_object("vid3") - - if 0: - self.pipeline = Pipeline(liveVideoXid=vid3.window.xid, - musicTime=self.musicTime, - recordingTo=self.recordingTo) - else: - self.pipeline = remotepivideo.Pipeline(liveVideo=vid3, - musicTime=self.musicTime, - recordingTo=self.recordingTo, - graph=graph) - - vid3.props.width_request = 360 - vid3.props.height_request = 220 - wtree.get_object("frame1").props.height_request = 220 - - self.pipeline.setInput('v4l') # auto seems to not search for dv - - gobject.timeout_add(1000 // framerate, self.updateLoop) - - def snapshot(self): - return self.pipeline.snapshot() - - def attachLog(self, textBuffer): - """write log lines to this gtk buffer""" - - class ToBuffer(logging.Handler): - - def emit(self, record): - textBuffer.set_text(record.getMessage()) - - h = ToBuffer() - h.setLevel(logging.INFO) - log.addHandler(h) - - def updateLoop(self): - position = self.musicTime.getLatest() - try: - with gtk.gdk.lock: - self.replayViews.update(position) - except Exception: - traceback.print_exc() - return True - - def getInputs(self): - return ['auto', 'dv', 'video0'] - - def on_liveVideoEnabled_toggled(self, widget): - self.pipeline.setLiveVideo(widget.get_active()) - - def on_liveFrameRate_value_changed(self, widget): - print(widget.get_value()) - - def onMusicTimeChange(self, position): - self.ignoreScaleChanges = True - try: - self.musicScale.set_range(0, position['duration']) - self.musicScale.set_value(position['t']) - finally: - self.ignoreScaleChanges = False - - def onMusicScaleValue(self, scaleRange): - """the scale position has changed. if it was by the user, send - it back to music player""" - if not self.ignoreScaleChanges: - self.musicTime.sendTime(scaleRange.get_value()) - - def incomingTime(self, t, source): - self.musicTime.lastHoverTime = t diff -r 623836db99af -r 4556eebe5d73 light9/vidref/moviestore.py --- a/light9/vidref/moviestore.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,106 +0,0 @@ -import os -from bisect import bisect_left -from rdflib import URIRef -import sys -sys.path.append( - '/home/drewp/Downloads/moviepy/lib/python2.7/site-packages') # for moviepy -from moviepy.video.io.ffmpeg_writer import FFMPEG_VideoWriter -from moviepy.video.io.ffmpeg_reader import FFMPEG_VideoReader - - -class _ResourceDir(object): - """the disk files for a resource""" - - def __init__(self, root, uri): - self.root, self.uri = root, uri - u = self.uri.replace('http://', '').replace('/', '_') - self.topDir = os.path.join(self.root, u) - try: - os.makedirs(self.topDir) - except OSError: - pass - - def videoPath(self): - return os.path.join(self.topDir, 'video.avi') - - def indexPath(self): - return os.path.join(self.topDir, 'frame_times') - - -class Writer(object): - """saves a video of a resource, receiving a frame at a time. Frame timing does not have to be regular.""" - - def __init__(self, root, uri): - self.rd = _ResourceDir(root, uri) - self.ffmpegWriter = None # lazy since we don't know the size yet - self.index = open(self.rd.indexPath(), 'w') - self.framesWritten = 0 - - def save(self, t, img): - if self.ffmpegWriter is None: - self.ffmpegWriter = FFMPEG_VideoWriter( - filename=self.rd.videoPath(), - size=img.size, - fps=10, # doesn't matter, just for debugging playbacks - codec='libx264') - self.ffmpegWriter.write_frame(img) - self.framesWritten = self.framesWritten + 1 - self.index.write('%d %g\n' % (self.framesWritten, t)) - - def close(self): - if self.ffmpegWriter is not None: - self.ffmpegWriter.close() - self.index.close() - - -class Reader(object): - - def __init__(self, resourceDir): - self.timeFrame = [] - for line in open(resourceDir.indexPath()): - f, t = line.strip().split() - self.timeFrame.append((float(t), int(f))) - self._reader = FFMPEG_VideoReader(resourceDir.videoPath()) - - def getFrame(self, t): - i = bisect_left(self.timeFrame, (t, None)) - i = min(i, len(self.timeFrame) - 1) - f = self.timeFrame[i][1] - return self._reader.get_frame(f) - - -class MultiReader(object): - """loads the nearest existing frame of a resource's video. Supports random access of multiple resources.""" - - def __init__(self, root): - self.root = root - # these should cleanup when they haven't been used in a while - self.readers = {} # uri: Reader - - def getFrame(self, uri, t): - if uri not in self.readers: - #self.readers.close all and pop them - self.readers[uri] = Reader(_ResourceDir(self.root, uri)) - return self.readers[uri].getFrame(t) - - -if __name__ == '__main__': - from PIL import Image - take = URIRef( - 'http://light9.bigasterisk.com/show/dance2015/song10/1434249076/') - if 0: - w = Writer('/tmp/ms', take) - for fn in sorted( - os.listdir( - '/home/drewp/light9-vidref/play-light9.bigasterisk.com_show_dance2015_song10/1434249076' - )): - t = float(fn.replace('.jpg', '')) - jpg = Image.open( - '/home/drewp/light9-vidref/play-light9.bigasterisk.com_show_dance2015_song10/1434249076/%08.03f.jpg' - % t) - jpg = jpg.resize((450, 176)) - w.save(t, jpg) - w.close() - else: - r = MultiReader('/tmp/ms') - print((r.getFrame(take, 5.6))) diff -r 623836db99af -r 4556eebe5d73 light9/vidref/remotepivideo.py --- a/light9/vidref/remotepivideo.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,143 +0,0 @@ -""" -like videorecorder.py, but talks to a bin/picamserve instance -""" -import os, time, logging -import gtk -import numpy -import treq -from twisted.internet import defer -from light9.vidref.replay import songDir, takeDir, snapshotDir -from light9 import showconfig -from light9.namespaces import L9 -from PIL import Image -from io import StringIO -log = logging.getLogger('remotepi') - - -class Pipeline(object): - - def __init__(self, liveVideo, musicTime, recordingTo, graph): - self.musicTime = musicTime - self.recordingTo = recordingTo - - self.liveVideo = self._replaceLiveVideoWidget(liveVideo) - - self._snapshotRequests = [] - self.graph = graph - self.graph.addHandler(self.updateCamUrl) - - def updateCamUrl(self): - show = showconfig.showUri() - self.picsUrl = self.graph.value(show, L9['vidrefCamRequest']) - log.info("picsUrl now %r", self.picsUrl) - if not self.picsUrl: - return - - # this cannot yet survive being called a second time - self._startRequest(str(self.picsUrl)) - - def _replaceLiveVideoWidget(self, liveVideo): - aspectFrame = liveVideo.get_parent() - liveVideo.destroy() - img = gtk.Image() - img.set_visible(True) - #img.set_size_request(320, 240) - aspectFrame.add(img) - return img - - def _startRequest(self, url): - self._buffer = '' - log.info('start request to %r', url) - d = treq.get(url) - d.addCallback(treq.collect, self._dataReceived) - # not sure how to stop this - return d - - def _dataReceived(self, chunk): - self._buffer += chunk - if len(self._buffer) < 100: - return - i = self._buffer.index('\n') - size, frameTime = self._buffer[:i].split() - size = int(size) - if len(self._buffer) - i - 1 < size: - return - jpg = self._buffer[i + 1:i + 1 + size] - self.onFrame(jpg, float(frameTime)) - self._buffer = self._buffer[i + 1 + size:] - - def snapshot(self): - """ - returns deferred to the path (which is under snapshotDir()) where - we saved the image. - """ - filename = "%s/%s.jpg" % (snapshotDir(), time.time()) - d = defer.Deferred() - self._snapshotRequests.append((d, filename)) - return d - - def setInput(self, name): - pass - - def setLiveVideo(self, on): - print("setLiveVideo", on) - - def onFrame(self, jpg, frameTime): - # We could pass frameTime here to try to compensate for lag, - # but it ended up looking worse in a test. One suspect is the - # rpi clock drift might be worse than the lag. The value of - # (now - frameTime) stutters regularly between 40ms, 140ms, - # and 200ms. - position = self.musicTime.getLatest() - - for d, filename in self._snapshotRequests: - with open(filename, 'w') as out: - out.write(jpg) - d.callback(filename) - self._snapshotRequests[:] = [] - - if not position['song']: - self.updateLiveFromTemp(jpg) - return - outDir = takeDir(songDir(position['song']), position['started']) - outFilename = "%s/%08.03f.jpg" % (outDir, position['t']) - if os.path.exists(outFilename): # we're paused on one time - self.updateLiveFromTemp(jpg) - return - try: - os.makedirs(outDir) - except OSError: - pass - with open(outFilename, 'w') as out: - out.write(jpg) - - self.updateLiveFromFile(outFilename) - - # if you're selecting the text while gtk is updating it, - # you can get a crash in xcb_io - if getattr(self, '_lastRecText', None) != outDir: - with gtk.gdk.lock: - self.recordingTo.set_text(outDir) - self._lastRecText = outDir - - def updateLiveFromFile(self, outFilename): - self.liveVideo.set_from_file(outFilename) - - def updateLiveFromTemp(self, jpg): - try: - img = Image.open(StringIO(jpg)) - if not hasattr(self, 'livePixBuf'): - self.livePixBuf = gtk.gdk.pixbuf_new_from_data( - img.tobytes(), gtk.gdk.COLORSPACE_RGB, False, 8, - img.size[0], img.size[1], img.size[0] * 3) - log.info("live images are %r", img.size) - else: - # don't leak pixbufs; update the one we have - a = self.livePixBuf.pixel_array - newImg = numpy.fromstring(img.tobytes(), dtype=numpy.uint8) - a[:, :, :] = newImg.reshape(a.shape) - self.liveVideo.set_from_pixbuf(self.livePixBuf) - - except Exception: - import traceback - traceback.print_exc() diff -r 623836db99af -r 4556eebe5d73 light9/vidref/setup.html --- a/light9/vidref/setup.html Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ - - - - vidref setup - - - - - - - - - - - Live: - - - - - diff -r 623836db99af -r 4556eebe5d73 light9/vidref/videorecorder.py --- a/light9/vidref/videorecorder.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,338 +0,0 @@ -from dataclasses import dataclass -from io import BytesIO -from typing import Optional -import time, logging, os, traceback - -import gi -gi.require_version('Gst', '1.0') -gi.require_version('GstBase', '1.0') - -from gi.repository import Gst -from rdflib import URIRef -from rx.subject import BehaviorSubject -from twisted.internet import threads -import PIL.Image -import moviepy.editor -import numpy - -from light9 import showconfig -from light9.ascoltami.musictime_client import MusicTime -from light9.newtypes import Song -from light9.metrics import metrics -log = logging.getLogger() - - -@dataclass -class CaptureFrame: - img: PIL.Image - song: Song - t: float - isPlaying: bool - imgJpeg: Optional[bytes] = None - - @metrics('jpeg_encode').time() - def asJpeg(self): - if not self.imgJpeg: - output = BytesIO() - self.img.save(output, 'jpeg', quality=80) - self.imgJpeg = output.getvalue() - return self.imgJpeg - - -def songDir(song: Song) -> bytes: - return os.path.join( - showconfig.root(), b'video', - song.replace('http://', '').replace('/', '_').encode('ascii')) - - -def takeUri(songPath: bytes) -> URIRef: - p = songPath.decode('ascii').split('/') - take = p[-1].replace('.mp4', '') - song = p[-2].split('_') - return URIRef('/'.join( - ['http://light9.bigasterisk.com/show', song[-2], song[-1], take])) - - -def deleteClip(uri: URIRef): - # uri http://light9.bigasterisk.com/show/dance2019/song6/take_155 - # path show/dance2019/video/light9.bigasterisk.com_show_dance2019_song6/take_155.* - w = uri.split('/')[-4:] - path = '/'.join([ - w[0], w[1], 'video', f'light9.bigasterisk.com_{w[0]}_{w[1]}_{w[2]}', - w[3] - ]) - log.info(f'deleting {uri} {path}') - metrics('deletes').incr() - for fn in [path + '.mp4', path + '.timing']: - os.remove(fn) - - -class FramesToVideoFiles: - """ - - nextWriteAction: 'ignore' - currentOutputClip: None - - (frames come in for new video) - nextWriteAction: 'saveFrame' - currentOutputClip: new VideoClip - (many frames) - - (music stops or song changes) - nextWriteAction: 'close' - currentOutputClip: None - nextWriteAction: 'ignore' - - """ - - def __init__(self, frames: BehaviorSubject, root: bytes): - self.frames = frames - self.root = root - self.nextImg: Optional[CaptureFrame] = None - - self.currentOutputClip: Optional[moviepy.editor.VideoClip] = None - self.currentOutputSong: Optional[Song] = None - self.nextWriteAction = 'ignore' - self.frames.subscribe(on_next=self.onFrame) - - def onFrame(self, cf: Optional[CaptureFrame]): - if cf is None: - return - self.nextImg = cf - - if self.currentOutputClip is None and cf.isPlaying: - # start up - self.nextWriteAction = 'saveFrames' - self.currentOutputSong = cf.song - self.save( - os.path.join(songDir(cf.song), b'take_%d' % int(time.time()))) - elif self.currentOutputClip and cf.isPlaying: - self.nextWriteAction = 'saveFrames' - # continue recording this - elif self.currentOutputClip is None and not cf.isPlaying: - self.nextWriteAction = 'notWritingClip' - pass # continue waiting - elif self.currentOutputClip and not cf.isPlaying or self.currentOutputSong != cf.song: - # stop - self.nextWriteAction = 'close' - else: - raise NotImplementedError(str(vars())) - - def save(self, outBase): - """ - receive frames (infinite) and wall-to-song times (stream ends with - the song), and write a video file and a frame map - """ - return threads.deferToThread(self._bg_save, outBase) - - def _bg_save(self, outBase: bytes): - os.makedirs(os.path.dirname(outBase), exist_ok=True) - self.frameMap = open(outBase + b'.timing', 'wt') - - # todo: see moviestore.py for a better-looking version where - # we get to call write_frame on a FFMPEG_VideoWriter instead - # of it calling us back. - - self.currentClipFrameCount = 0 - - # (immediately calls make_frame) - self.currentOutputClip = moviepy.editor.VideoClip(self._bg_make_frame, - duration=999.) - # The fps recorded in the file doesn't matter much; we'll play - # it back in sync with the music regardless. - self.currentOutputClip.fps = 10 - log.info(f'write_videofile {outBase} start') - try: - self.outMp4 = outBase.decode('ascii') + '.mp4' - self.currentOutputClip.write_videofile(self.outMp4, - codec='libx264', - audio=False, - preset='ultrafast', - logger=None, - ffmpeg_params=['-g', '10'], - bitrate='150000') - except (StopIteration, RuntimeError): - self.frameMap.close() - - log.info('write_videofile done') - self.currentOutputClip = None - - if self.currentClipFrameCount < 400: - log.info('too small- deleting') - deleteClip(takeUri(self.outMp4.encode('ascii'))) - - def _bg_make_frame(self, video_time_secs): - metrics('encodeFrameFps').incr() - if self.nextWriteAction == 'close': - raise StopIteration # the one in write_videofile - elif self.nextWriteAction == 'notWritingClip': - raise NotImplementedError - elif self.nextWriteAction == 'saveFrames': - pass - else: - raise NotImplementedError(self.nextWriteAction) - - # should be a little queue to miss fewer frames - t1 = time.time() - while self.nextImg is None: - time.sleep(.015) - metrics('wait_for_next_img').observe(time.time() - t1) - cf, self.nextImg = self.nextImg, None - - self.frameMap.write(f'video {video_time_secs:g} = song {cf.t:g}\n') - self.currentClipFrameCount += 1 - return numpy.asarray(cf.img) - - -class GstSource: - - def __init__(self, dev): - """ - make new gst pipeline - """ - Gst.init(None) - self.musicTime = MusicTime(pollCurvecalc=False) - self.liveImages: BehaviorSubject = BehaviorSubject( - None) # stream of Optional[CaptureFrame] - - # need to use 640,480 on some webcams or they fail mysteriously - size = [800, 600] - - log.info("new pipeline using device=%s" % dev) - - # using videocrop breaks the pipeline, may be this issue - # https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/issues/732 - pipeStr = ( - f"v4l2src device=\"{dev}\"" - # f'autovideosrc' - f" ! videoconvert" - f" ! appsink emit-signals=true max-buffers=1 drop=true name=end0 caps=video/x-raw,format=RGB,width={size[0]},height={size[1]}" - ) - log.info("pipeline: %s" % pipeStr) - - self.pipe = Gst.parse_launch(pipeStr) - - self.setupPipelineError(self.pipe, self.onError) - - self.appsink = self.pipe.get_by_name('end0') - self.appsink.connect('new-sample', self.new_sample) - - self.pipe.set_state(Gst.State.PLAYING) - log.info('gst pipeline is recording video') - - def new_sample(self, appsink): - try: - sample = appsink.emit('pull-sample') - caps = sample.get_caps() - buf = sample.get_buffer() - (result, mapinfo) = buf.map(Gst.MapFlags.READ) - try: - img = PIL.Image.frombytes( - 'RGB', (caps.get_structure(0).get_value('width'), - caps.get_structure(0).get_value('height')), - mapinfo.data) - img = self.crop(img) - finally: - buf.unmap(mapinfo) - # could get gst's frame time and pass it to getLatest - latest = self.musicTime.getLatest() - if 'song' in latest: - metrics('queue_gst_frame_fps').incr() - self.liveImages.on_next( - CaptureFrame(img=img, - song=Song(latest['song']), - t=latest['t'], - isPlaying=latest['playing'])) - except Exception: - traceback.print_exc() - return Gst.FlowReturn.OK - - @metrics('crop').time() - def crop(self, img): - return img.crop((40, 100, 790, 310)) - - def setupPipelineError(self, pipe, cb): - bus = pipe.get_bus() - - def onBusMessage(bus, msg): - - print('nusmsg', msg) - if msg.type == Gst.MessageType.ERROR: - _, txt = msg.parse_error() - cb(txt) - return True - - # not working; use GST_DEBUG=4 to see errors - bus.add_watch(0, onBusMessage) - bus.connect('message', onBusMessage) - - def onError(self, messageText): - if ('v4l2src' in messageText and - ('No such file or directory' in messageText or - 'Resource temporarily unavailable' in messageText or - 'No such device' in messageText)): - log.error(messageText) - os.abort() - else: - log.error("ignoring error: %r" % messageText) - - -''' -class oldPipeline(object): - - def __init__(self): - self.snapshotRequests = Queue() - - def snapshot(self): - """ - returns deferred to the path (which is under snapshotDir()) where - we saved the image. This callback comes from another thread, - but I haven't noticed that being a problem yet. - """ - d = defer.Deferred() - - def req(frame): - filename = "%s/%s.jpg" % ('todo', time.time()) - log.debug("received snapshot; saving in %s", filename) - frame.save(filename) - d.callback(filename) - - log.debug("requesting snapshot") - self.snapshotRequests.put(req) - return d - - - self.imagesToSave = Queue() - self.startBackgroundImageSaver(self.imagesToSave) - - def startBackgroundImageSaver(self, imagesToSave): - """do image saves in another thread to not block gst""" - - def imageSaver(): - while True: - args = imagesToSave.get() - self.saveImg(*args) - imagesToSave.task_done() - - # this is not an ideal place for snapshotRequests - # since imagesToSave is allowed to get backed up with - # image writes, yet we would still want the next new - # image to be used for the snapshot. chainfunc should - # put snapshot images in a separate-but-similar queue - # to imagesToSave, and then another watcher could use - # those to satisfy snapshot requests - try: - req = self.snapshotRequests.get(block=False) - except Empty: - pass - else: - req(args[1]) - self.snapshotRequests.task_done() - - t = Thread(target=imageSaver) - t.setDaemon(True) - t.start() - - def chainfunc(self, pad, buffer): - position = self.musicTime.getLatest() -''' diff -r 623836db99af -r 4556eebe5d73 light9/vidref/vidref.glade --- a/light9/vidref/vidref.glade Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,487 +0,0 @@ - - - - - - False - vidref - 690 - 500 - - - True - False - - - True - False - - - True - False - - - 450 - 277 - True - False - 0 - out - - - True - False - 0 - none - 1.3300000429153442 - - - 320 - 240 - True - False - - - - - - - True - False - <b>Live view</b> - True - - - - - False - True - 0 - - - - - True - False - - - Enabled - 110 - 36 - True - True - True - False - True - - - - False - True - 0 - - - - - True - False - - - 75 - 20 - True - False - Frame rate: - - - False - True - 0 - - - - - 52 - 25 - True - True - - False - False - True - True - True - - - True - True - 1 - - - - - False - True - 1 - - - - - True - False - - - 85 - 20 - True - False - Input source: - - - False - True - 0 - - - - - 100 - True - False - - - True - True - 1 - - - - - False - True - 2 - - - - - True - False - - - True - False - Recording -to: - - - True - True - 0 - - - - - True - True - False - char - recordingTo - - - True - True - 1 - - - - - True - True - 3 - - - - - True - False - 0 - none - - - True - True - char - lastLog - - - - - True - False - <b>Last log</b> - True - - - - - True - True - 4 - - - - - False - True - 1 - - - - - False - True - 0 - - - - - 336 - 259 - True - False - 0 - out - - - 571 - 367 - True - True - automatic - automatic - out - - - True - False - queue - - - True - False - - - - - - - - - - - - - - - - - - True - False - <b>Playback 1</b> - True - - - - - True - True - 1 - - - - - True - True - 0 - - - - - True - False - 0 - none - - - True - False - 12 - - - True - True - 2 - - - - - - - True - False - <b>Music position</b> - True - - - - - False - True - 1 - - - - - - - True - False - gtk-delete - - - - /home/drewp/light9-vidref/play-light9.bigasterisk.com_show_dance2010_song6/1276582699 - - - False - - - True - False - - - 320 - 240 - True - False - 0 - out - 1.3300000429153442 - - - 320 - 240 - True - False - gtk-missing-image - - - - - False - True - 0 - - - - - True - False - - - True - False - - - True - False - Started: - - - False - False - 0 - - - - - True - True - False - - 12 - Sat 14:22:25 - False - False - True - True - - - False - False - 1 - - - - - False - True - 0 - - - - - Enabled - True - True - True - False - - - False - True - 1 - - - - - Delete - True - True - True - False - image2 - - - False - True - 2 - - - - - Pin to top - True - True - False - False - True - - - False - True - 3 - - - - - False - True - 1 - - - - - - diff -r 623836db99af -r 4556eebe5d73 light9/vidref/vidref.html --- a/light9/vidref/vidref.html Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,189 +0,0 @@ - - - - vidref - - - - - -

video setup

- -
Camera view
-
-
- -
-
-
- -
- set these -
-
-
-
-
-
-
-
- - - - - - - - - -
- -
- -
Resulting url:
- -
Resulting crop image:
-
- - - - - - - - - diff -r 623836db99af -r 4556eebe5d73 light9/vidref/vidref.ui --- a/light9/vidref/vidref.ui Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,234 +0,0 @@ - - - MainWindow - - - - 0 - 0 - 863 - 728 - - - - MainWindow - - - - - - 20 - 260 - 251 - 16 - - - - Live view - - - - - - 20 - 10 - 320 - 240 - - - - - - - 50 - 280 - 121 - 19 - - - - enabled - - - true - - - - - - 50 - 470 - 171 - 121 - - - - - song - - - - - time - - - - - value - - - - - whatever - - - - - - - 40 - 340 - 52 - 13 - - - - Song - - - - - - 40 - 360 - 52 - 13 - - - - Time - - - - - - 90 - 330 - 113 - 23 - - - - - - - 90 - 360 - 113 - 23 - - - - - - - 570 - 330 - 191 - 191 - - - - true - - - - - 0 - 0 - 189 - 189 - - - - - - - - 270 - 310 - 411 - 331 - - - - Replay from 16:10 - - - - - 20 - 30 - 311 - 231 - - - - - - - 60 - 270 - 191 - 19 - - - - follow current time - - - graphicsView_2 - graphicsView_2 - checkBox_2 - - - - - - 0 - 0 - 863 - 21 - - - - - - - toolBar - - - TopToolBarArea - - - false - - - - - toolBar_2 - - - TopToolBarArea - - - false - - - - - - - startLiveView() - - diff -r 623836db99af -r 4556eebe5d73 light9/wavelength.py --- a/light9/wavelength.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,19 +0,0 @@ -#!/usr/bin/python - -import sys, wave - - -def wavelength(filename): - filename = filename.replace('.ogg', '.wav') - wavefile = wave.open(filename, 'rb') - - framerate = wavefile.getframerate() # frames / second - nframes = wavefile.getnframes() # number of frames - song_length = nframes / framerate - - return song_length - - -if __name__ == "__main__": - for songfile in sys.argv[1:]: - print(songfile, wavelength(songfile)) diff -r 623836db99af -r 4556eebe5d73 light9/wavepoints.py --- a/light9/wavepoints.py Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,43 +0,0 @@ -import wave, audioop - - -def simp(filename, seconds_per_average=0.001): - """smaller seconds_per_average means fewer data points""" - wavefile = wave.open(filename, 'rb') - print("# gnuplot data for %s, seconds_per_average=%s" % - (filename, seconds_per_average)) - print( - "# %d channels, samplewidth: %d, framerate: %s, frames: %d\n# Compression type: %s (%s)" - % wavefile.getparams()) - - framerate = wavefile.getframerate() # frames / second - - frames_to_read = int(framerate * seconds_per_average) - print("# frames_to_read=%s" % frames_to_read) - - time_and_max = [] - values = [] - count = 0 - while True: - fragment = wavefile.readframes(frames_to_read) - if not fragment: - break - - # other possibilities: - # m = audioop.avg(fragment, 2) - # print count, "%s %s" % audioop.minmax(fragment, 2) - - m = audioop.rms(fragment, wavefile._framesize) - time_and_max.append((count, m)) - values.append(m) - count += frames_to_read - # if count>1000000: - # break - - # find the min and max - min_value, max_value = min(values), max(values) - points = [] # (secs,height) - for count, value in time_and_max: - points.append( - (count / framerate, (value - min_value) / (max_value - min_value))) - return points diff -r 623836db99af -r 4556eebe5d73 light9/web/AutoDependencies.ts --- a/light9/web/AutoDependencies.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,137 +0,0 @@ -import debug from "debug"; -import { NamedNode, Quad_Graph, Quad_Object, Quad_Predicate, Quad_Subject, Term, Util } from "n3"; -import { filter } from "underscore"; -import { Patch, QuadPattern } from "./patch"; -import { SubEvent } from "sub-events"; -import { SyncedGraph } from "./SyncedGraph"; - -const log = debug("autodep"); - -// use patch as an optional optimization, but you can't count on it -export type HandlerFunc = (p?: Patch) => void; - -class Handler { - patterns: QuadPattern[]; - innerHandlers: Handler[]; - // a function and the quad patterns it cared about - constructor(public func: HandlerFunc | null, public label: string) { - this.patterns = []; // s,p,o,g quads that should trigger the next run - this.innerHandlers = []; // Handlers requested while this one was running - } -} - -export class AutoDependencies { - handlers: Handler; - handlerStack: Handler[]; - graphError: SubEvent = new SubEvent(); - constructor(private graph: SyncedGraph) { - // tree of all known Handlers (at least those with non-empty - // patterns). Top node is not a handler. - this.handlers = new Handler(null, "root"); - this.handlerStack = [this.handlers]; // currently running - log("window.ad"); - (window as any).ad = this; - } - - runHandler(func: HandlerFunc, label: string) { - // what if we have this func already? duplicate is safe? - if (label == null) { - throw new Error("missing label"); - } - - const h = new Handler(func, label); - const tailChildren = this.handlerStack[this.handlerStack.length - 1].innerHandlers; - const matchingLabel = filter(tailChildren, (c: Handler) => c.label === label).length; - // ohno, something depends on some handlers getting run twice :( - if (matchingLabel < 2) { - tailChildren.push(h); - } - //console.time("handler #{label}") - // todo: this may fire 1-2 times before the - // graph is initially loaded, which is a waste. Try deferring it if we - // haven't gotten the graph yet. - this._rerunHandler(h, /*patch=*/ undefined); - log(`new handler ${label} ran first time and requested ${h.patterns.length} pats`); - } - - _rerunHandler(handler: Handler, patch?: Patch) { - handler.patterns = []; - this.handlerStack.push(handler); - try { - if (handler.func === null) { - throw new Error("tried to rerun root"); - } - handler.func(patch); - } catch (e) { - this.graphError.emit(String(e)); - } finally { - // assuming here it didn't get to do all its queries, we could - // add a *,*,*,* handler to call for sure the next time? - // log('done. got: ', handler.patterns) - this.handlerStack.pop(); - } - } - - // handler might have no watches, in which case we could forget about it - logHandlerTree() { - log("handler tree:"); - const shorten = (x: Term | null) => { - if (x === null) { - return "null"; - } - if (!Util.isNamedNode(x)) { - return x.value; - } - return this.graph.shorten(x as NamedNode); - }; - - var prn = (h: Handler, indent: string) => { - log(`${indent} 🤝 handler "${h.label}" ${h.patterns.length} pats`); - for (let pat of h.patterns) { - log(`${indent} ⣝ s=${shorten(pat.subject)} p=${shorten(pat.predicate)} o=${shorten(pat.object)}`); - } - Array.from(h.innerHandlers).map((c: any) => prn(c, indent + " ")); - }; - prn(this.handlers, ""); - } - - _handlerIsAffected(child: Handler, patch: Patch): boolean { - // it should be correct but slow to always return true here - for (let pat of child.patterns) { - if (patch.matches(pat)) { - return true; - } - } - return false; - } - - graphChanged(patch: Patch) { - // SyncedGraph is telling us this patch just got applied to the graph. - - var rerunInners = (cur: Handler) => { - const toRun = cur.innerHandlers.slice(); - for (let child of Array.from(toRun)) { - const match = this._handlerIsAffected(child, patch); - - if (match) { - log("match", child.label, match); - child.innerHandlers = []; // let all children get called again - this._rerunHandler(child, patch); - } else { - rerunInners(child); - } - } - }; - rerunInners(this.handlers); - } - - askedFor(s: Quad_Subject | null, p: Quad_Predicate | null, o: Quad_Object | null, g: Quad_Graph | null) { - // SyncedGraph is telling us someone did a query that depended on - // quads in the given pattern. - // console.log(` asked for s/${s?.id} p/${p?.id} o/${o?.id}`) - const current = this.handlerStack[this.handlerStack.length - 1]; - if (current != null && current !== this.handlers) { - current.patterns.push({ subject: s, predicate: p, object: o, graph: g } as QuadPattern); - } - } -} diff -r 623836db99af -r 4556eebe5d73 light9/web/EditChoice.ts --- a/light9/web/EditChoice.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,118 +0,0 @@ -// see light9/editchoice.py for gtk version -import debug from "debug"; -import { css, html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { NamedNode } from "n3"; -import { $V, Vector } from "sylvester"; -export { ResourceDisplay } from "../web/ResourceDisplay"; -const log = debug("editchoice"); -const RDFS = "http://www.w3.org/2000/01/rdf-schema#"; - -function setupDrop( - senseElem: HTMLElement, - highlightElem: HTMLElement, - coordinateOriginElem: HTMLElement | null, - onDrop: (uri: NamedNode, pos: Vector | null) => void -) { - const highlight = () => highlightElem.classList.add("dragging"); - const unhighlight = () => highlightElem.classList.remove("dragging"); - - senseElem.addEventListener("drag", (event: DragEvent) => { }); - - senseElem.addEventListener("dragstart", (event: DragEvent) => { }); - - senseElem.addEventListener("dragend", (event: DragEvent) => { }); - - senseElem.addEventListener("dragover", (event: DragEvent) => { - event.preventDefault(); - event.dataTransfer!.dropEffect = "copy"; - highlight(); - }); - - senseElem.addEventListener("dragenter", (event: DragEvent) => { - highlight(); - }); - - senseElem.addEventListener("dragleave", (event: DragEvent) => { - unhighlight(); - }); - - senseElem.addEventListener("drop", (event: DragEvent) => { - event.preventDefault(); - const uri = new NamedNode(event.dataTransfer!.getData("text/uri-list")); - - let pos: Vector | null = null; - if (coordinateOriginElem != null) { - const root = coordinateOriginElem.getBoundingClientRect(); - pos = $V([event.pageX - root.left, event.pageY - root.top]); - } - - try { - onDrop(uri, pos); - } catch (e) { - log(e); - } - unhighlight(); - }); -} - -// Picks a URI based on the caller setting the property OR -// the user drag-and-dropping a text/uri-list resource (probably -// an or tag) -@customElement("edit-choice") -export class EditChoice extends LitElement { - @property() uri?: NamedNode - @property({ type: Boolean }) nounlink = false; - @property({ type: Boolean }) rename = false; - static styles = [ - css` - :host { - display: inline-block; - background: #141448; - /* min-width: 10em; */ - padding: 3px 8px; - } - .dragging { - background: rgba(126, 52, 245, 0.0784313725490196); - box-shadow: 0 0 20px #ffff00; - } - a { - color: #8e8eff; - padding: 3px; - display: inline-block; - font-size: 145%; - } - `, - ]; - render() { - const unlink = html` - - ` - return html` - - ${this.nounlink ? html`` : unlink} - `; - } - - constructor() { - super(); - setupDrop(this, this, null, this._setUri.bind(this)); - } - - // updated(changedProperties: PropertyValues) { - // log('cp' ,changedProperties) - // if (changedProperties.has("box")) { - // log('setupdrop', this.box) - // setupDrop(this.box, this.box, null, this._setUri.bind(this)); - // } - // } - - _setUri(u?: NamedNode) { - this.uri = u; - this.dispatchEvent(new CustomEvent("edited", { detail: { newValue: u } })); - } - - unlink() { - return this._setUri(undefined); - } -} diff -r 623836db99af -r 4556eebe5d73 light9/web/Light9CursorCanvas.ts --- a/light9/web/Light9CursorCanvas.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,146 +0,0 @@ -import debug from "debug"; -import { css, html, LitElement, PropertyValues } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import Sylvester from "sylvester"; -import { line } from "./drawing"; - -const $V = Sylvester.Vector.create; - -const log = debug("cursor"); - -export interface PlainViewState { - zoomSpec: { t1: () => number; t2: () => number }; - fullZoomX: (t: number) => number; - zoomInX: (t: number) => number; - cursor: { t: () => number }; - audioY: () => number; - audioH: () => number; - zoomedTimeY: () => number; // not what you think- it's the zone in between - zoomedTimeH: () => number; - mouse: { pos: () => Vector }; -} - -// For cases where you have a zoomed-out view on top of a zoomed-in view, -// overlay this element and it'll draw a time cursor on both views. -@customElement("light9-cursor-canvas") -export class Light9CursorCanvas extends LitElement { - cursorPath: null | { - top0: Vector; - top1: Vector; - mid0: Vector; - mid1: Vector; - mid2: Vector; - mid3: Vector; - bot0: Vector; - bot1: Vector; - } = null; - canvasEl!: HTMLCanvasElement; - ctx!: CanvasRenderingContext2D; - offsetWidth: any; - offsetHeight: any; - @property() viewState: PlainViewState | null = null; - static styles = [ - css` - :host { - display: inline-block; - } - `, - ]; - render() { - return html``; - } - - updated(changedProperties: PropertyValues) { - if (changedProperties.has("viewState")) { - this.redrawCursor(); - } - } - connectedCallback() { - super.connectedCallback(); - window.addEventListener("resize", this.onResize); - this.onResize(); - } - - firstUpdated() { - this.canvasEl = this.shadowRoot!.firstElementChild as HTMLCanvasElement; - this.onResize(); - this.ctx = this.canvasEl.getContext("2d")!; - } - - disconnectedCallback() { - window.removeEventListener("resize", this.onResize); - super.disconnectedCallback(); - } - - // onViewState() { - // ko.computed(this.redrawCursor.bind(this)); - // } - - onResize() { - if (!this.canvasEl) { - return; - } - this.canvasEl.width = this.offsetWidth; - this.canvasEl.height = this.offsetHeight; - this.redrawCursor(); - } - - redrawCursor() { - const vs = this.viewState; - if (!vs) { - return; - } - const dependOn = [vs.zoomSpec.t1(), vs.zoomSpec.t2()]; - const xZoomedOut = vs.fullZoomX(vs.cursor.t()); - const xZoomedIn = vs.zoomInX(vs.cursor.t()); - - this.cursorPath = { - top0: $V([xZoomedOut, vs.audioY()]), - top1: $V([xZoomedOut, vs.audioY() + vs.audioH()]), - mid0: $V([xZoomedIn + 2, vs.zoomedTimeY() + vs.zoomedTimeH()]), - mid1: $V([xZoomedIn - 2, vs.zoomedTimeY() + vs.zoomedTimeH()]), - mid2: $V([xZoomedOut - 1, vs.audioY() + vs.audioH()]), - mid3: $V([xZoomedOut + 1, vs.audioY() + vs.audioH()]), - bot0: $V([xZoomedIn, vs.zoomedTimeY() + vs.zoomedTimeH()]), - bot1: $V([xZoomedIn, this.offsetHeight]), - }; - this.redraw(); - } - - redraw() { - if (!this.ctx || !this.viewState) { - return; - } - this.ctx.clearRect(0, 0, this.canvasEl.width, this.canvasEl.height); - - this.ctx.strokeStyle = "#fff"; - this.ctx.lineWidth = 0.5; - this.ctx.beginPath(); - const mouse = this.viewState.mouse.pos(); - line(this.ctx, $V([0, mouse.e(2)]), $V([this.canvasEl.width, mouse.e(2)])); - line(this.ctx, $V([mouse.e(1), 0]), $V([mouse.e(1), this.canvasEl.height])); - this.ctx.stroke(); - - if (this.cursorPath) { - this.ctx.strokeStyle = "#ff0303"; - this.ctx.lineWidth = 1.5; - this.ctx.beginPath(); - line(this.ctx, this.cursorPath.top0, this.cursorPath.top1); - this.ctx.stroke(); - - this.ctx.fillStyle = "#9c0303"; - this.ctx.beginPath(); - this.ctx.moveTo(this.cursorPath.mid0.e(1), this.cursorPath.mid0.e(2)); - for (let p of [this.cursorPath.mid1, this.cursorPath.mid2, this.cursorPath.mid3]) { - this.ctx.lineTo(p.e(1), p.e(2)); - } - this.ctx.fill(); - - this.ctx.strokeStyle = "#ff0303"; - this.ctx.lineWidth = 3; - this.ctx.beginPath(); - line(this.ctx, this.cursorPath.bot0, this.cursorPath.bot1, "#ff0303", "3px"); - this.ctx.stroke(); - } - } -} diff -r 623836db99af -r 4556eebe5d73 light9/web/RdfDbChannel.ts --- a/light9/web/RdfDbChannel.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,160 +0,0 @@ -import debug from "debug"; -import { SubEvent } from "sub-events"; -import { SyncgraphPatchMessage } from "./patch"; -const log = debug("rdfdbclient"); - -class ChannelPinger { - private timeoutId?: number; - private lastMs: number = 0; - constructor(private ws: WebSocket) { - this._pingLoop(); - } - lastPingMs(): number { - return this.lastMs; - } - pong() { - this.lastMs = Date.now() + this.lastMs; - } - _pingLoop() { - if (this.ws.readyState !== this.ws.OPEN) { - return; - } - this.ws.send("PING"); - this.lastMs = -Date.now(); - - if (this.timeoutId != null) { - clearTimeout(this.timeoutId); - } - this.timeoutId = (setTimeout(this._pingLoop.bind(this), 10000) as unknown) as number; - } -} - -export class RdfDbChannel { - // lower level reconnecting websocket -- knows about message types, but not what's inside a patch body - private ws?: WebSocket = undefined; - private pinger?: ChannelPinger; - private connectionId: string = "none"; // server's name for us - private reconnectTimer?: NodeJS.Timeout = undefined; - private messagesReceived = 0; // (non-ping messages) - private messagesSent = 0; - - newConnection: SubEvent = new SubEvent(); - serverMessage: SubEvent<{ evType: string; body: SyncgraphPatchMessage }> = new SubEvent(); - statusDisplay: SubEvent = new SubEvent(); - - constructor(public patchSenderUrl: string) { - this.openConnection(); - } - sendMessage(body: string): boolean { - // one try, best effort, true if we think it worked - if (!this.ws || this.ws.readyState !== this.ws.OPEN) { - return false; - } - log("send patch to server, " + body.length + " bytes"); - this.ws.send(body); - this.messagesSent++; - this.updateStatus(); - return true; - } - - disconnect(why:string) { - // will be followed by an autoconnect - log("disconnect requested:", why); - if (this.ws !== undefined) { - const closeHandler = this.ws.onclose?.bind(this.ws); - if (!closeHandler) { - throw new Error(); - } - closeHandler(new CloseEvent("forced")); - } - } - - private openConnection() { - const wsOrWss = window.location.protocol.replace("http", "ws"); - const fullUrl = wsOrWss + "//" + window.location.host + this.patchSenderUrl; - if (this.ws !== undefined) { - this.ws.close(); - } - this.ws = new WebSocket(fullUrl); - this.ws.onopen = this.onWsOpen.bind(this, this.ws); - this.ws.onerror = this.onWsError.bind(this); - this.ws.onclose = this.onWsClose.bind(this); - this.ws.onmessage = this.onWsMessage.bind(this); - } - - private onWsOpen(ws: WebSocket) { - log("new connection to", this.patchSenderUrl); - this.updateStatus(); - this.newConnection.emit(); - this.pinger = new ChannelPinger(ws); - } - - private onWsMessage(evt: { data: string }) { - const msg = evt.data; - if (msg === "PONG") { - this.onPong(); - return; - } - this.onJson(msg); - } - - private onPong() { - if (this.pinger) { - this.pinger.pong(); - this.updateStatus(); - } - } - - private onJson(msg: string) { - const input = JSON.parse(msg); - if (input.connectedAs) { - this.connectionId = input.connectedAs; - } else { - this.onPatch(input as SyncgraphPatchMessage); - } - } - - private onPatch(input: SyncgraphPatchMessage) { - log(`patch msg from server`); - this.serverMessage.emit({ evType: "patch", body: input }); - this.messagesReceived++; - this.updateStatus(); - } - - private onWsError(e: Event) { - log("ws error", e); - this.disconnect("ws error"); - this.updateStatus(); - } - - private onWsClose(ev: CloseEvent) { - log("ws close"); - this.updateStatus(); - if (this.reconnectTimer !== undefined) { - clearTimeout(this.reconnectTimer); - } - this.reconnectTimer = setTimeout(this.openConnection.bind(this), 1000); - } - - private updateStatus() { - const conn = (() => { - if (this.ws === undefined) { - return "no"; - } else { - switch (this.ws.readyState) { - case this.ws.CONNECTING: - return "connecting"; - case this.ws.OPEN: - return `open as ${this.connectionId}`; - case this.ws.CLOSING: - return "closing"; - case this.ws.CLOSED: - return "close"; - } - } - })(); - - const ping = this.pinger ? this.pinger.lastPingMs() : "..."; - this.statusDisplay.emit(`${conn}; ${this.messagesReceived} recv; ${this.messagesSent} sent; ping ${ping}ms`); - } -} diff -r 623836db99af -r 4556eebe5d73 light9/web/RdfdbSyncedGraph.ts --- a/light9/web/RdfdbSyncedGraph.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,62 +0,0 @@ -import debug from "debug"; -import { LitElement, css, html } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { SyncedGraph } from "./SyncedGraph"; - -const log = debug("syncedgraph-el"); - -// Contains a SyncedGraph. Displays as little status box. -// Put one element on your page and use getTopGraph everywhere. -@customElement("rdfdb-synced-graph") -export class RdfdbSyncedGraph extends LitElement { - @property() graph: SyncedGraph; - @property() status: string; - @property() testGraph = false; - static styles = [ - css` - :host { - display: inline-block; - border: 1px solid gray; - min-width: 22em; - background: #05335a; - color: #4fc1d4; - } - `, - ]; - render() { - return html`graph: ${this.status}`; - } - - constructor() { - super(); - this.status = "startup"; - const prefixes = new Map([ - ["", "http://light9.bigasterisk.com/"], - ["dev", "http://light9.bigasterisk.com/device/"], - ["effect", "http://light9.bigasterisk.com/effect/"], - ["rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"], - ["rdfs", "http://www.w3.org/2000/01/rdf-schema#"], - ["xsd", "http://www.w3.org/2001/XMLSchema#"], - ]); - this.graph = new SyncedGraph( - this.testGraph ? "unused" : "/service/rdfdb/syncedGraph", - prefixes, - (s: string) => { - this.status = s; - } - ); - setTopGraph(this.graph); - } -} - -// todo: consider if this has anything to contribute: -// https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md -let setTopGraph: (sg: SyncedGraph) => void; -(window as any).topSyncedGraph = new Promise((res, rej) => { - setTopGraph = res; -}); - -export async function getTopGraph(): Promise { - const s = (window as any).topSyncedGraph; - return await s; -} diff -r 623836db99af -r 4556eebe5d73 light9/web/ResourceDisplay.ts --- a/light9/web/ResourceDisplay.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,164 +0,0 @@ -import { TextField } from "@material/mwc-textfield"; -import debug from "debug"; -import { css, html, LitElement, PropertyValues } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; -import { NamedNode } from "n3"; -import { getTopGraph } from "./RdfdbSyncedGraph"; -import { SyncedGraph } from "./SyncedGraph"; -export { Button } from "@material/mwc-button"; -export { Dialog } from "@material/mwc-dialog"; -export { TextField } from "@material/mwc-textfield"; - -const log = debug("rdisplay"); - -@customElement("resource-display") -export class ResourceDisplay extends LitElement { - graph!: SyncedGraph; - static styles = [ - css` - :host { - display: inline-block; - } - - a.resource { - color: inherit; - text-decoration: none; - } - - .resource { - border: 1px solid #545454; - border-radius: 5px; - padding: 1px; - margin: 2px; - background: rgb(49, 49, 49); - display: inline-block; - text-shadow: 1px 1px 2px black; - } - .resource.minor { - background: none; - border: none; - } - .resource a { - color: rgb(150, 150, 255); - padding: 1px; - display: inline-block; - } - .resource.minor a { - text-decoration: none; - color: rgb(155, 155, 193); - padding: 0; - } - `, - ]; - - render() { - let renameDialog = html``; - if (this.renameDialogOpen) { - renameDialog = html` -

- New label: - -

- Cancel - OK -
`; - } - - return html` -
${this.label} - ${this.rename ? html`` : ""} ${renameDialog}`; - // - } - @property() uri?: NamedNode; - - @state() label: string = ""; - @state() renameDialogOpen = false; - @state() renameTo = ""; - - @property({ type: Boolean }) rename: boolean = false; - @property({ type: Boolean }) noclick: boolean = false; - @property({ type: Boolean }) minor: boolean = false; - - constructor() { - super(); - getTopGraph().then((g) => { - this.graph = g; - this.onUri(); - }); - } - - updated(changedProperties: PropertyValues) { - if (changedProperties.has("uri")) { - this.onUri(); - } - } - - private onUri() { - if (!this.graph) { - return; /*too soon, but getTopGraph will call us again*/ - } - - if (this.uri === undefined) { - this.label = "(unset)"; - } else if (this.uri === null) { - throw 'use undefined please' - } else { - this.graph.runHandler(this.compile.bind(this, this.graph), `label for ${this.uri.id}`); - } - } - private compile(graph: SyncedGraph) { - if (this.uri === undefined) { - return; - } else { - this.label = this.graph.labelOrTail(this.uri); - } - } - - private href(): string { - if (!this.uri || this.noclick) { - return "javascript:;"; - } - return this.uri.value; - } - - private resClasses() { - return this.minor ? "resource minor" : "resource"; - } - - private onRename() { - this.renameTo = this.label; - this.renameDialogOpen = true; - setTimeout(() => { - // I! 👏 know! 👏 the! 👏 element! 👏 I! 👏 want! - const inputEl = this.shadowRoot!.querySelector("#renameField")!.shadowRoot!.querySelector("input")! as HTMLInputElement; - inputEl.setSelectionRange(0, -1); - }, 100); - } - - // move to SyncedGraph - private whatCtxHeldTheObj(subj: NamedNode, pred: NamedNode): NamedNode { - var ctxs = this.graph.contextsWithPattern(subj, pred, null); - if (ctxs.length != 1) { - throw new Error(`${ctxs.length} ${pred.id} stmts for ${subj.id}`); - } - return ctxs[0]; - } - - private onRenameClosing(ev: CustomEvent) { - this.renameTo = (this.shadowRoot!.querySelector("#renameField")! as TextField).value; - } - - private onRenameClosed(ev: CustomEvent) { - this.renameDialogOpen = false; - if (ev.detail.action == "ok") { - var label = this.graph.Uri("rdfs:label"); - if (this.uri === undefined) { - throw "lost uri"; - } - const ctx = this.whatCtxHeldTheObj(this.uri, label); - this.graph.patchObject(this.uri, label, this.graph.Literal(this.renameTo), ctx); - } - this.renameTo = ""; - } -} diff -r 623836db99af -r 4556eebe5d73 light9/web/SyncedGraph.ts --- a/light9/web/SyncedGraph.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,404 +0,0 @@ -import debug from "debug"; -import * as N3 from "n3"; -import { Quad, Quad_Object, Quad_Predicate, Quad_Subject } from "n3"; -import { sortBy, unique } from "underscore"; -import { AutoDependencies, HandlerFunc } from "./AutoDependencies"; -import { Patch, patchToDeleteEntireGraph } from "./patch"; -import { RdfDbClient } from "./rdfdbclient"; - -const log = debug("graph"); - -const RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"; - -export class SyncedGraph { - private autoDeps: AutoDependencies; - private client: RdfDbClient; - private graph: N3.Store; - private cachedFloatValues: Map = new Map(); - private cachedUriValues: Map = new Map(); - private prefixFuncs: (prefix: string) => N3.PrefixedToIri; - private serial: any; - private nextNumber: any; - // Main graph object for a browser to use. Consider using RdfdbSyncedGraph element to create & own - // one of these. Syncs both ways with rdfdb. Meant to hide the choice of RDF lib, so we can change it - // later. - // - // Note that _applyPatch is the only method to write to the graph, so - // it can fire subscriptions. - - constructor( - // The /syncedGraph path of an rdfdb server. - patchSenderUrl: string, - // prefixes can be used in Uri(curie) calls. This mapping may grow during loadTrig calls. - public prefixes: Map, - private setStatus: (status: string) => void - ) { - this.prefixFuncs = this.rebuildPrefixFuncs(prefixes); - this.graph = new N3.Store(); - this.autoDeps = new AutoDependencies(this); - this.autoDeps.graphError.subscribe((e) => { - log("graph learned of error - reconnecting", e); - this.client.disconnect("graph error"); - }); - this.clearGraph(); - - this.client = new RdfDbClient(patchSenderUrl, this._clearGraphOnNewConnection.bind(this), this._applyPatch.bind(this), this.setStatus); - } - - clearGraph() { - // must not try send a patch to the server! - // just deletes the statements; watchers are unaffected. - this.cachedFloatValues = new Map(); // s + '|' + p -> number - this.cachedUriValues = new Map(); // s + '|' + p -> Uri - - const p = patchToDeleteEntireGraph(this.graph); - if (!p.isEmpty()) { - this._applyPatch(p); - } - // if we had a Store already, this lets N3.Store free all its indices/etc - this.graph = new N3.Store(); - this.rebuildPrefixFuncs(this.prefixes); - } - - _clearGraphOnNewConnection() { - // must not try send a patch to the server - - log("clearGraphOnNewConnection"); - this.clearGraph(); - log("clearGraphOnNewConnection done"); - } - - private rebuildPrefixFuncs(prefixes: Map) { - const p = Object.create(null); - prefixes.forEach((v: string, k: string) => (p[k] = v)); - - this.prefixFuncs = N3.Util.prefixes(p); - return this.prefixFuncs; - } - - U() { - // just a shorthand - return this.Uri.bind(this); - } - - Uri(curie: string) { - if (curie == null) { - throw new Error("no uri"); - } - if (curie.match(/^http/)) { - return N3.DataFactory.namedNode(curie); - } - const part = curie.split(":"); - return this.prefixFuncs(part[0])(part[1]); - } - - // Uri(shorten(u)).value==u - shorten(uri: N3.NamedNode): string { - for (let row of [ - { sh: "dev", lo: "http://light9.bigasterisk.com/theater/vet/device/" }, - { sh: "effect", lo: "http://light9.bigasterisk.com/effect/" }, - { sh: "", lo: "http://light9.bigasterisk.com/" }, - { sh: "rdfs", lo: "http://www.w3.org/2000/01/rdf-schema#" }, - { sh: "xsd", lo: "http://www.w3.org/2001/XMLSchema#" }, - ]) { - if (uri.value.startsWith(row.lo)) { - return row.sh + ":" + uri.value.substring(row.lo.length); - } - } - return uri.value; - } - - Literal(jsValue: string | number) { - return N3.DataFactory.literal(jsValue); - } - - LiteralRoundedFloat(f: number) { - return N3.DataFactory.literal(f.toPrecision(3), this.Uri("http://www.w3.org/2001/XMLSchema#decimal")); - } - - Quad(s: any, p: any, o: any, g: any) { - return N3.DataFactory.quad(s, p, o, g); - } - - toJs(literal: { value: any }) { - // incomplete - return parseFloat(literal.value); - } - - loadTrig(trig: any, cb: () => any) { - // for debugging - const adds: Quad[] = []; - const parser = new N3.Parser(); - parser.parse(trig, (error: any, quad: any, prefixes: any) => { - if (error) { - throw new Error(error); - } - if (quad) { - adds.push(quad); - } else { - this._applyPatch(new Patch([], adds)); - // todo: here, add those prefixes to our known set - if (cb) { - cb(); - } - } - }); - } - - quads(): any { - // for debugging - return Array.from(this.graph.getQuads(null, null, null, null)).map((q: Quad) => [q.subject, q.predicate, q.object, q.graph]); - } - - applyAndSendPatch(patch: Patch) { - console.time("applyAndSendPatch"); - if (!this.client) { - log("not connected-- dropping patch"); - return; - } - if (!patch.isEmpty()) { - this._applyPatch(patch); - // // chaos delay - // setTimeout(()=>{ - if (this.client) { - log("sending patch:\n", patch.dump()); - this.client.sendPatch(patch); - } - // },300*Math.random()) - } - console.timeEnd("applyAndSendPatch"); - } - - _applyPatch(patch: Patch) { - // In most cases you want applyAndSendPatch. - // - // This is the only method that writes to this.graph! - if (patch.isEmpty()) throw "dont send empty patches here"; - log("_applyPatch [1] \n", patch.dump()); - this.cachedFloatValues.clear(); - this.cachedUriValues.clear(); - patch.applyToGraph(this.graph); - if (false) { - log("applied patch locally", patch.summary()); - } else { - log("applied patch locally:\n" + patch.dump()); - } - this.autoDeps.graphChanged(patch); - } - - getObjectPatch(s: N3.NamedNode, p: N3.NamedNode, newObject: N3.Quad_Object | null, g: N3.NamedNode): Patch { - // make a patch which removes existing values for (s,p,*,c) and - // adds (s,p,newObject,c). Values in other graphs are not affected. - const existing = this.graph.getQuads(s, p, null, g); - return new Patch(existing, newObject !== null ? [this.Quad(s, p, newObject, g)] : []); - } - - patchObject(s: N3.NamedNode, p: N3.NamedNode, newObject: N3.Quad_Object | null, g: N3.NamedNode) { - this.applyAndSendPatch(this.getObjectPatch(s, p, newObject, g)); - } - - clearObjects(s: N3.NamedNode, p: N3.NamedNode, g: N3.NamedNode) { - this.applyAndSendPatch(new Patch(this.graph.getQuads(s, p, null, g), [])); - } - - public runHandler(func: HandlerFunc, label: string) { - // runs your func once, tracking graph calls. if a future patch - // matches what you queried, we runHandler your func again (and - // forget your queries from the first time). - - // helps with memleak? not sure yet. The point was if two matching - // labels get puushed on, we should run only one. So maybe - // appending a serial number is backwards. - if (!this.serial) { - this.serial = 1; - } - this.serial += 1; - //label = label + @serial - - this.autoDeps.runHandler(func, label); - } - - _singleValue(s: Quad_Subject, p: Quad_Predicate) { - this.autoDeps.askedFor(s, p, null, null); - const quads = this.graph.getQuads(s, p, null, null); - const objs = new Set(Array.from(quads).map((q: Quad) => q.object)); - - switch (objs.size) { - case 0: - throw new Error("no value for " + s.value + " " + p.value); - case 1: - var obj = objs.values().next().value; - return obj; - default: - throw new Error("too many different values: " + JSON.stringify(quads)); - } - } - - floatValue(s: Quad_Subject, p: Quad_Predicate) { - const key = s.value + "|" + p.value; - const hit = this.cachedFloatValues.get(key); - if (hit !== undefined) { - return hit; - } - //log('float miss', s, p) - - const v = this._singleValue(s, p).value; - const ret = parseFloat(v); - if (isNaN(ret)) { - throw new Error(`${s.value} ${p.value} -> ${v} not a float`); - } - this.cachedFloatValues.set(key, ret); - return ret; - } - - stringValue(s: any, p: any) { - return this._singleValue(s, p).value; - } - - uriValue(s: Quad_Subject, p: Quad_Predicate) { - const key = s.value + "|" + p.value; - const hit = this.cachedUriValues.get(key); - if (hit !== undefined) { - return hit; - } - - const ret = this._singleValue(s, p); - this.cachedUriValues.set(key, ret); - return ret; - } - - labelOrTail(uri: { value: { split: (arg0: string) => any } }) { - let ret: any; - try { - ret = this.stringValue(uri, this.Uri("rdfs:label")); - } catch (error) { - const words = uri.value.split("/"); - ret = words[words.length - 1]; - } - if (!ret) { - ret = uri.value; - } - return ret; - } - - objects(s: any, p: any): Quad_Object[] { - this.autoDeps.askedFor(s, p, null, null); - const quads = this.graph.getQuads(s, p, null, null); - return Array.from(quads).map((q: { object: any }) => q.object); - } - - subjects(p: any, o: any): Quad_Subject[] { - this.autoDeps.askedFor(null, p, o, null); - const quads = this.graph.getQuads(null, p, o, null); - return Array.from(quads).map((q: { subject: any }) => q.subject); - } - - subjectStatements(s: Quad_Subject): Quad[] { - this.autoDeps.askedFor(s, null, null, null); - const quads = this.graph.getQuads(s, null, null, null); - return quads; - } - - items(list: any) { - const out = []; - let current = list; - while (true) { - if (current.value === RDF + "nil") { - break; - } - - this.autoDeps.askedFor(current, null, null, null); // a little loose - - const firsts = this.graph.getQuads(current, RDF + "first", null, null); - const rests = this.graph.getQuads(current, RDF + "rest", null, null); - if (firsts.length !== 1) { - throw new Error(`list node ${current} has ${firsts.length} rdf:first edges`); - } - out.push(firsts[0].object); - - if (rests.length !== 1) { - throw new Error(`list node ${current} has ${rests.length} rdf:rest edges`); - } - current = rests[0].object; - } - - return out; - } - - contains(s: any, p: any, o: any): boolean { - this.autoDeps.askedFor(s, p, o, null); - // Sure this is a nice warning to remind me to rewrite, but the graph.size call itself was taking 80% of the time in here - // log("contains calling getQuads when graph has ", this.graph.size); - return this.graph.getQuads(s, p, o, null).length > 0; - } - - nextNumberedResources(base: { id: any }, howMany: number) { - // base is NamedNode or string - // Note this is unsafe before we're synced with the graph. It'll - // always return 'name0'. - if (base.id) { - base = base.id; - } - const results = []; - - // @contains is really slow. - if (this.nextNumber == null) { - this.nextNumber = new Map(); - } - let start = this.nextNumber.get(base); - if (start === undefined) { - start = 0; - } - - for (let serial = start, asc = start <= 1000; asc ? serial <= 1000 : serial >= 1000; asc ? serial++ : serial--) { - const uri = this.Uri(`${base}${serial}`); - if (!this.contains(uri, null, null)) { - results.push(uri); - log("nextNumberedResources", `picked ${uri}`); - this.nextNumber.set(base, serial + 1); - if (results.length >= howMany) { - return results; - } - } - } - throw new Error(`can't make sequential uri with base ${base}`); - } - - nextNumberedResource(base: any) { - return this.nextNumberedResources(base, 1)[0]; - } - - contextsWithPattern(s: Quad_Subject | null, p: Quad_Predicate | null, o: Quad_Object | null): N3.NamedNode[] { - this.autoDeps.askedFor(s, p, o, null); - const ctxs: N3.NamedNode[] = []; - for (let q of Array.from(this.graph.getQuads(s, p, o, null))) { - if (q.graph.termType != "NamedNode") throw `context was ${q.graph.id}`; - ctxs.push(q.graph); - } - return unique(ctxs); - } - - sortKey(uri: N3.NamedNode) { - const parts = uri.value.split(/([0-9]+)/); - const expanded = parts.map(function (p: string) { - const f = parseInt(p); - if (isNaN(f)) { - return p; - } - return p.padStart(8, "0"); - }); - return expanded.join(""); - } - - sortedUris(uris: any) { - return sortBy(uris, this.sortKey); - } - - prettyLiteral(x: any) { - if (typeof x === "number") { - return this.LiteralRoundedFloat(x); - } else { - return this.Literal(x); - } - } -} diff -r 623836db99af -r 4556eebe5d73 light9/web/TiledHome.ts --- a/light9/web/TiledHome.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,49 +0,0 @@ -import * as React from "react"; -import { createRoot } from "react-dom/client"; -import * as FlexLayout from "flexlayout-react"; -export { Light9DeviceSettings } from "./live/Light9DeviceSettings"; -export { Light9CollectorUi } from "./collector/Light9CollectorUi"; - -const config:FlexLayout.IJsonModel = { - global: {}, - borders: [], - layout: { - type: "row", - weight: 100, - children: [ - { - type: "tabset", - weight: 50, - children: [{ type: "tab", name: "devsettings", component: "light9-device-settings" }], - }, - { - type: "tabset", - weight: 50, - children: [{ type: "tab", name: "collector", component: "light9-collector-ui" }], - }, - ], - }, -}; - -const e = React.createElement; - -// see https://github.com/lit/lit/tree/main/packages/labs/react - -class Main extends React.Component { - constructor(props) { - super(props); - this.state = { model: FlexLayout.Model.fromJson(config) }; - } - - factory = (node) => { - var component = node.getComponent(); - return e(component, null, ""); - }; - - render() { - return e(FlexLayout.Layout, { model: this.state.model, factory: this.factory }); - } -} - -const root = createRoot(document.getElementById("container")!); -root.render(React.createElement(Main)); diff -r 623836db99af -r 4556eebe5d73 light9/web/ascoltami/Light9AscoltamiUi.ts --- a/light9/web/ascoltami/Light9AscoltamiUi.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,310 +0,0 @@ -import debug from "debug"; -import { css, html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { classMap } from "lit/directives/class-map.js"; -import { NamedNode } from "n3"; -import Sylvester from "sylvester"; -import { Zoom } from "../light9-timeline-audio"; -import { PlainViewState } from "../Light9CursorCanvas"; -import { getTopGraph } from "../RdfdbSyncedGraph"; -import { SyncedGraph } from "../SyncedGraph"; -import { TimingUpdate } from "./main"; -import { showRoot } from "../show_specific"; -export { Light9TimelineAudio } from "../light9-timeline-audio"; -export { Light9CursorCanvas } from "../Light9CursorCanvas"; -export { RdfdbSyncedGraph } from "../RdfdbSyncedGraph"; -export { ResourceDisplay } from "../ResourceDisplay"; -const $V = Sylvester.Vector.create; - -debug.enable("*"); -const log = debug("asco"); - -function byId(id: string): HTMLElement { - return document.getElementById(id)!; -} -async function postJson(url: string, jsBody: Object) { - return fetch(url, { - method: "POST", - headers: { "Content-Type": "applcation/json" }, - body: JSON.stringify(jsBody), - }); -} -@customElement("light9-ascoltami-ui") -export class Light9AscoltamiUi extends LitElement { - graph!: SyncedGraph; - times!: { intro: number; post: number }; - @property() nextText: string = ""; - @property() isPlaying: boolean = false; - @property() show: NamedNode | null = null; - @property() song: NamedNode | null = null; - @property() selectedSong: NamedNode | null = null; - @property() currentDuration: number = 0; - @property() zoom: Zoom; - @property() overviewZoom: Zoom; - @property() viewState: PlainViewState | null = null; - static styles = [ - css` - :host { - display: flex; - flex-direction: column; - } - .timeRow { - margin: 14px; - position: relative; - } - #overview { - height: 60px; - } - #zoomed { - margin-top: 40px; - height: 80px; - } - #cursor { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - } - #grow { - flex: 1 1 auto; - display: flex; - } - #grow > span { - display: flex; - position: relative; - width: 50%; - } - #playSelected { - height: 100px; - } - #songList { - overflow-y: scroll; - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - } - #songList .row { - width: 60%; - min-height: 40px; - text-align: left; - position: relative; - } - #songList .row:nth-child(even) { - background: #333; - } - #songList .row:nth-child(odd) { - background: #444; - } - #songList button { - min-height: 40px; - margin-bottom: 10px; - } - #songList .row.playing { - box-shadow: 0 0 30px red; - background-color: #de5050; - } - `, - ]; - render() { - return html` - - - - - -
- -
- - ${this.songList.map( - (song) => html` - - - - - ` - )} -
- -
-
- - -
- -
-
- - - -
- -
- - - - - -
`; - } - - onSelectSong(song: NamedNode, ev: MouseEvent) { - if (this.selectedSong && song.equals(this.selectedSong)) { - this.selectedSong = null; - } else { - this.selectedSong = song; - } - } - async onPlaySelected(ev: Event) { - if (!this.selectedSong) { - return; - } - await fetch("../service/ascoltami/song", { method: "POST", body: this.selectedSong.value }); - } - - onCmdStop(ev?: MouseEvent): void { - postJson("../service/ascoltami/time", { pause: true }); - } - onCmdPlay(ev?: MouseEvent): void { - postJson("../service/ascoltami/time", { resume: true }); - } - onCmdIntro(ev?: MouseEvent): void { - postJson("../service/ascoltami/time", { t: this.times.intro, resume: true }); - } - onCmdPost(ev?: MouseEvent): void { - postJson("../service/ascoltami/time", { - t: this.currentDuration - this.times.post, - resume: true, - }); - } - onCmdGo(ev?: MouseEvent): void { - postJson("../service/ascoltami/go", {}); - } - - bindKeys() { - document.addEventListener("keypress", (ev) => { - if (ev.which == 115) { - this.onCmdStop(); - return false; - } - if (ev.which == 112) { - this.onCmdPlay(); - return false; - } - if (ev.which == 105) { - this.onCmdIntro(); - return false; - } - if (ev.which == 116) { - this.onCmdPost(); - return false; - } - - if (ev.key == "g") { - this.onCmdGo(); - return false; - } - return true; - }); - } - - async musicSetup() { - // shoveled over from the vanillajs version - const config = await (await fetch("../service/ascoltami/config")).json(); - this.show = new NamedNode(config.show); - this.times = config.times; - document.title = document.title.replace("{{host}}", config.host); - try { - const h1 = document.querySelector("h1")!; - h1.innerText = h1.innerText.replace("{{host}}", config.host); - } catch (e) {} - - (window as any).finishOldStyleSetup(this.times, this.onOldStyleUpdate.bind(this)); - } - - onOldStyleUpdate(data: TimingUpdate) { - this.nextText = data.next; - this.isPlaying = data.playing; - this.currentDuration = data.duration; - this.song = new NamedNode(data.song); - this.overviewZoom = { duration: data.duration, t1: 0, t2: data.duration }; - const t1 = data.t - 2, - t2 = data.t + 20; - this.zoom = { duration: data.duration, t1, t2 }; - const timeRow = this.shadowRoot!.querySelector(".timeRow") as HTMLDivElement; - const w = timeRow.offsetWidth; - this.viewState = { - zoomSpec: { t1: () => t1, t2: () => t2 }, - cursor: { t: () => data.t }, - audioY: () => 0, - audioH: () => 60, - zoomedTimeY: () => 60, - zoomedTimeH: () => 40, - fullZoomX: (sec: number) => (sec / data.duration) * w, - zoomInX: (sec: number) => ((sec - t1) / (t2 - t1)) * w, - mouse: { pos: () => $V([0, 0]) }, - }; - } - - @property() songList: NamedNode[] = []; - constructor() { - super(); - this.bindKeys(); - this.zoom = this.overviewZoom = { duration: null, t1: 0, t2: 1 }; - - getTopGraph().then((g) => { - this.graph = g; - this.musicSetup(); // async - this.graph.runHandler(this.graphChanged.bind(this), "loadsongs"); - }); - } - graphChanged() { - this.songList = []; - try { - const playList = this.graph.uriValue( - // - this.graph.Uri(showRoot), - this.graph.Uri(":playList") - ); - log(playList); - this.songList = this.graph.items(playList) as NamedNode[]; - } catch (e) { - log("no playlist yet"); - } - log(this.songList.length); - } -} diff -r 623836db99af -r 4556eebe5d73 light9/web/ascoltami/index.html --- a/light9/web/ascoltami/index.html Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,83 +0,0 @@ - - - - ascoltami on {{host}} - - - - - - -
-

ascoltami on {{host}}

- - -
- - - - - - - - - - - - -
- Song: -
Time: Left: - Until autostop: - -
- -
-
- -
- -

reload

-
- - - diff -r 623836db99af -r 4556eebe5d73 light9/web/ascoltami/main.ts --- a/light9/web/ascoltami/main.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,100 +0,0 @@ -function byId(id: string): HTMLElement { - return document.getElementById(id)!; -} - -export interface TimingUpdate { - // GET /ascoltami/time response - duration: number; - next: string; // e.g. 'play' - playing: boolean; - song: string; - started: number; // unix sec - t: number; // seconds into song - state: { current: { name: string }; pending: { name: string } }; -} - -(window as any).finishOldStyleSetup = async (times: { intro: number; post: number }, timingUpdate: (data: TimingUpdate) => void) => { - let currentHighlightedSong = ""; - // let lastPlaying = false; - - - const events = new EventSource("../service/ascoltami/time/stream"); - events.addEventListener("message", (m)=>{ - const update = JSON.parse(m.data) as TimingUpdate - updateCurrent(update) - markUpdateTiming(); - }) - - async function updateCurrent(data:TimingUpdate) { - byId("currentSong").innerText = data.song; - if (data.song != currentHighlightedSong) { - showCurrentSong(data.song); - } - byId("currentTime").innerText = data.t.toFixed(1); - byId("leftTime").innerText = (data.duration - data.t).toFixed(1); - byId("leftAutoStopTime").innerText = Math.max(0, data.duration - times.post - data.t).toFixed(1); - byId("states").innerText = JSON.stringify(data.state); - // document.querySelector("#timeSlider").slider({ value: data.t, max: data.duration }); - timingUpdate(data); - } - let recentUpdates: Array = []; - function markUpdateTiming() { - recentUpdates.push(+new Date()); - recentUpdates = recentUpdates.slice(Math.max(recentUpdates.length - 5, 0)); - } - - function refreshUpdateFreqs() { - if (recentUpdates.length > 1) { - if (+new Date() - recentUpdates[recentUpdates.length - 1] > 1000) { - byId("updateActual").innerText = "(stalled)"; - return; - } - - var avgMs = (recentUpdates[recentUpdates.length - 1] - recentUpdates[0]) / (recentUpdates.length - 1); - byId("updateActual").innerText = "" + Math.round(1000 / avgMs); - } - } - setInterval(refreshUpdateFreqs, 2000); - - function showCurrentSong(uri: string) { - document.querySelectorAll(".songs div").forEach((row: Element, i: number) => { - if (row.querySelector("button")!.dataset.uri == uri) { - row.classList.add("currentSong"); - } else { - row.classList.remove("currentSong"); - } - }); - currentHighlightedSong = uri; - } - - const data = await (await fetch("api/songs")).json(); - data.songs.forEach((song: { uri: string; label: string }) => { - const button = document.createElement("button"); - // link is just for dragging, not clicking - const link = document.createElement("a"); - const n = document.createElement("span"); - n.classList.add("num"); - n.innerText = song.label.slice(0, 2); - link.appendChild(n); - - const sn = document.createElement("span"); - sn.classList.add("song-name"); - sn.innerText = song.label.slice(2).trim(); - link.appendChild(sn); - link.setAttribute("href", song.uri); - link.addEventListener("click", (ev) => { - ev.stopPropagation(); - button.click(); - }); - button.appendChild(link); - button.dataset.uri = song.uri; - button.addEventListener("click", async (ev) => { - await fetch("api/song", { method: "POST", body: song.uri }); - showCurrentSong(song.uri); - }); - const dv = document.createElement("div"); - dv.appendChild(button); - document.querySelector(".songs")!.appendChild(dv); - }); - -}; diff -r 623836db99af -r 4556eebe5d73 light9/web/collector/Light9CollectorDevice.ts --- a/light9/web/collector/Light9CollectorDevice.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,75 +0,0 @@ -import debug from "debug"; -import { css, html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { NamedNode } from "n3"; -export { ResourceDisplay } from "../../web/ResourceDisplay"; - -const log = debug("device-el"); - -@customElement("light9-collector-device") -export class Light9CollectorDevice extends LitElement { - static styles = [ - css` - :host { - display: block; - break-inside: avoid-column; - font-size: 80%; - } - h3 { - margin-top: 12px; - margin-bottom: 0; - } - td { - white-space: nowrap; - } - - td.nonzero { - background: #310202; - color: #e25757; - } - td.full { - background: #2b0000; - color: red; - font-weight: bold; - } - `, - ]; - - render() { - return html` -

- - - - - - - ${this.attrs.map( - (item) => html` - - - - - - ` - )} -
out attrvaluechan
${item.attr}${item.val} →${item.chan}
- `; - } - @property({ - converter: acceptStringOrUri(), - }) - uri: NamedNode = new NamedNode(""); - @property() attrs: Array<{ attr: string; valClass: string; val: string; chan: string }> = []; - - setAttrs(attrs: any) { - this.attrs = attrs; - this.attrs.forEach(function (row: any) { - row.valClass = row.val == 255 ? "full" : row.val ? "nonzero" : ""; - }); - } -} - -function acceptStringOrUri() { - return (s: string | null) => new NamedNode(s || ""); -} diff -r 623836db99af -r 4556eebe5d73 light9/web/collector/Light9CollectorUi.ts --- a/light9/web/collector/Light9CollectorUi.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,91 +0,0 @@ -import debug from "debug"; -import { html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { NamedNode } from "n3"; -import ReconnectingWebSocket from "reconnectingwebsocket"; -import { sortBy, uniq } from "underscore"; -import { Patch } from "../patch"; -import { getTopGraph } from "../RdfdbSyncedGraph"; -import { SyncedGraph } from "../SyncedGraph"; -import { Light9CollectorDevice } from "./Light9CollectorDevice"; -export { RdfdbSyncedGraph } from "../RdfdbSyncedGraph"; -export { Light9CollectorDevice }; - -debug.enable("*"); -const log = debug("collector"); - -@customElement("light9-collector-ui") -export class Light9CollectorUi extends LitElement { - graph!: SyncedGraph; - render() { - return html` -

Collector [metrics]

- -

Devices

-
${this.devices.map((d) => html``)}
`; - } - - @property() devices: NamedNode[] = []; - - constructor() { - super(); - getTopGraph().then((g) => { - this.graph = g; - this.graph.runHandler(this.findDevices.bind(this), "findDevices"); - }); - - const ws = new ReconnectingWebSocket(location.href.replace("http", "ws") + "../service/collector/updates"); - ws.addEventListener("message", (ev: any) => { - const outputAttrsSet = JSON.parse(ev.data).outputAttrsSet; - if (outputAttrsSet) { - this.updateDev(outputAttrsSet.dev, outputAttrsSet.attrs); - } - }); - } - - findDevices(patch?: Patch) { - const U = this.graph.U(); - - this.devices = []; - this.clearDeviceChildElementCache(); - let classes = this.graph.subjects(U("rdf:type"), U(":DeviceClass")); - uniq(sortBy(classes, "value"), true).forEach((dc) => { - sortBy(this.graph.subjects(U("rdf:type"), dc), "value").forEach((dev) => { - this.devices.push(dev as NamedNode); - }); - }); - } - - deviceElements: Map = new Map(); - - clearDeviceChildElementCache() { - this.deviceElements = new Map(); - } - - findDeviceChildElement(uri: string): Light9CollectorDevice | undefined { - const known = this.deviceElements.get(uri); - if (known) { - return known; - } - - for (const el of this.shadowRoot!.querySelectorAll("light9-collector-device")) { - const eld = el as Light9CollectorDevice; - if (eld.uri.value == uri) { - this.deviceElements.set(uri, eld); - return eld; - } - } - - return undefined; - } - - updateDev(uri: string, attrs: { attr: string; chan: string; val: string; valClass: string }[]) { - const el = this.findDeviceChildElement(uri); - if (!el) { - // unresolved race: updates come in before we have device elements to display them - setTimeout(() => this.updateDev(uri, attrs), 300); - return; - } - el.setAttrs(attrs); - } -} diff -r 623836db99af -r 4556eebe5d73 light9/web/collector/index.html --- a/light9/web/collector/index.html Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,19 +0,0 @@ - - - - collector - - - - - - - - - - - diff -r 623836db99af -r 4556eebe5d73 light9/web/colorpick_crosshair_large.svg --- a/light9/web/colorpick_crosshair_large.svg Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,127 +0,0 @@ - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - diff -r 623836db99af -r 4556eebe5d73 light9/web/colorpick_crosshair_small.svg --- a/light9/web/colorpick_crosshair_small.svg Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,116 +0,0 @@ - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - diff -r 623836db99af -r 4556eebe5d73 light9/web/colorpick_rainbow_large.png Binary file light9/web/colorpick_rainbow_large.png has changed diff -r 623836db99af -r 4556eebe5d73 light9/web/colorpick_rainbow_small.png Binary file light9/web/colorpick_rainbow_small.png has changed diff -r 623836db99af -r 4556eebe5d73 light9/web/drawing.ts --- a/light9/web/drawing.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +0,0 @@ - -export function svgPathFromPoints(pts: { forEach: (arg0: (p: any) => void) => void }) { - let out = ""; - pts.forEach(function (p: Number[] | { elements: Number[] }) { - let x, y; - if ((p as any).elements) { - // for vec2 - [x, y] = (p as any).elements; - } else { - [x, y] = p as Number[]; - } - if (out.length === 0) { - out = "M "; - } else { - out += "L "; - } - out += "" + x + "," + y + " "; - }); - return out; -}; - -export function line( - ctx: { moveTo: (arg0: any, arg1: any) => void; lineTo: (arg0: any, arg1: any) => any }, - p1: { e: (arg0: number) => any }, - p2: { e: (arg0: number) => any } -) { - ctx.moveTo(p1.e(1), p1.e(2)); - return ctx.lineTo(p2.e(1), p2.e(2)); -}; - -// http://stackoverflow.com/a/4959890 -export function roundRect( - ctx: { - beginPath: () => void; - moveTo: (arg0: any, arg1: any) => void; - lineTo: (arg0: number, arg1: number) => void; - arc: (arg0: number, arg1: number, arg2: any, arg3: number, arg4: number, arg5: boolean) => void; - closePath: () => any; - }, - sx: number, - sy: number, - ex: number, - ey: number, - r: number -) { - const d2r = Math.PI / 180; - if (ex - sx - 2 * r < 0) { - r = (ex - sx) / 2; - } // ensure that the radius isn't too large for x - if (ey - sy - 2 * r < 0) { - r = (ey - sy) / 2; - } // ensure that the radius isn't too large for y - ctx.beginPath(); - ctx.moveTo(sx + r, sy); - ctx.lineTo(ex - r, sy); - ctx.arc(ex - r, sy + r, r, d2r * 270, d2r * 360, false); - ctx.lineTo(ex, ey - r); - ctx.arc(ex - r, ey - r, r, d2r * 0, d2r * 90, false); - ctx.lineTo(sx + r, ey); - ctx.arc(sx + r, ey - r, r, d2r * 90, d2r * 180, false); - ctx.lineTo(sx, sy + r); - ctx.arc(sx + r, sy + r, r, d2r * 180, d2r * 270, false); - return ctx.closePath(); -}; diff -r 623836db99af -r 4556eebe5d73 light9/web/edit-choice-demo.html --- a/light9/web/edit-choice-demo.html Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff -r 623836db99af -r 4556eebe5d73 light9/web/edit-choice.coffee --- a/light9/web/edit-choice.coffee Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,62 +0,0 @@ -log = debug('editchoice') -RDFS = 'http://www.w3.org/2000/01/rdf-schema#' - - - -window.setupDrop = (senseElem, highlightElem, coordinateOriginElem, onDrop) -> - - highlight = -> highlightElem.classList.add('dragging') - unhighlight = -> highlightElem.classList.remove('dragging') - - senseElem.addEventListener 'drag', (event) => - - senseElem.addEventListener 'dragstart', (event) => - - senseElem.addEventListener 'dragend', (event) => - - senseElem.addEventListener 'dragover', (event) => - event.preventDefault() - event.dataTransfer.dropEffect = 'copy' - highlight() - - senseElem.addEventListener 'dragenter', (event) => - highlight() - - senseElem.addEventListener 'dragleave', (event) => - unhighlight() - - senseElem.addEventListener 'drop', (event) -> - event.preventDefault() - uri = event.dataTransfer.getData('text/uri-list') - - pos = if coordinateOriginElem? - root = coordinateOriginElem.getBoundingClientRect() - $V([event.pageX - root.left, event.pageY - root.top]) - else - null - - try - onDrop(uri, pos) - catch e - log(e) - unhighlight() - - - -coffeeElementSetup(class EditChoice extends Polymer.Element - @is: "edit-choice", - @getter_properties: - graph: {type: Object, notify: true}, - uri: {type: String, notify: true}, - - _setUri: (u) -> - @uri = u - @dispatchEvent(new CustomEvent('edited')) - - connectedCallback: -> - super.connectedCallback() - setupDrop(@$.box, @$.box, null, @_setUri.bind(@)) - - unlink: -> - @_setUri(null) -) diff -r 623836db99af -r 4556eebe5d73 light9/web/edit-choice_test.html --- a/light9/web/edit-choice_test.html Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,58 +0,0 @@ - - - - edit-choice test - - - - - - - - - - - - - -
-
- - - -
- - - - diff -r 623836db99af -r 4556eebe5d73 light9/web/effects/Light9EffectListing.ts --- a/light9/web/effects/Light9EffectListing.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,113 +0,0 @@ -import debug from "debug"; -import { css, html, LitElement } from "lit"; -import { customElement } from "lit/decorators.js"; -import { NamedNode } from "n3"; -import { sortBy } from "underscore"; -import { getTopGraph } from "../RdfdbSyncedGraph"; -import { SyncedGraph } from "../SyncedGraph"; -export { ResourceDisplay } from "../ResourceDisplay"; - -debug.enable("*"); -const log = debug("listing"); - -@customElement("light9-effect-listing") -export class Light9EffectListing extends LitElement { - render() { - return html` -

Effects

- - - ${this.effects.map((e: NamedNode) => html``)} - `; - } - graph!: SyncedGraph; - effects: NamedNode[] = []; - - constructor() { - super(); - getTopGraph().then((g) => { - this.graph = g; - this.graph.runHandler(this.getClasses.bind(this), "getClasses"); - }); - } - - getClasses() { - const U = this.graph.U(); - this.effects = this.graph.subjects(U("rdf:type"), U(":Effect")) as NamedNode[]; - this.effects = sortBy(this.effects, (ec: NamedNode) => { - try { - return this.graph.stringValue(ec, U("rdfs:label")); - } catch (e) { - return ec.value; - } - }); - this.requestUpdate(); - } -} - -@customElement("light9-effect-class") -export class Light9EffectClass extends LitElement { - static styles = [ - css` - :host { - display: block; - padding: 5px; - border: 1px solid green; - background: #1e271e; - margin-bottom: 3px; - } - a { - color: #7992d0; - background: #00000859; - min-width: 4em; - min-height: 2em; - display: inline-block; - text-align: center; - vertical-align: middle; - } - resource-display { - min-width: 12em; - font-size: 180%; - } - `, - ]; - render() { - if (!this.uri) { - return html`loading...`; - } - return html` - Effect - - Edit - - - - - - `; - } - graph!: SyncedGraph; - uri?: NamedNode; - - onAdd() { - // this.$.songEffects.body = { drop: this.uri.value }; - // this.$.songEffects.generateRequest(); - } - - onMomentaryPress() { - // this.$.songEffects.body = { drop: this.uri.value, event: "start" }; - // this.lastPress = this.$.songEffects.generateRequest(); - // return this.lastPress.completes.then((request: { response: { note: any } }) => { - // return (this.lastMomentaryNote = request.response.note); - // }); - } - - onMomentaryRelease() { - // if (!this.lastMomentaryNote) { - // return; - // } - // this.$.songEffects.body = { drop: this.uri.value, note: this.lastMomentaryNote }; - // this.lastMomentaryNote = null; - // return this.$.songEffects.generateRequest(); - } -} diff -r 623836db99af -r 4556eebe5d73 light9/web/effects/index.html --- a/light9/web/effects/index.html Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,12 +0,0 @@ - - - - effect listing - - - - - - - - diff -r 623836db99af -r 4556eebe5d73 light9/web/fade/Light9EffectFader.ts --- a/light9/web/fade/Light9EffectFader.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,190 +0,0 @@ -import debug from "debug"; -import { css, html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; -import { NamedNode, Quad } from "n3"; -import { getTopGraph } from "../RdfdbSyncedGraph"; -import { showRoot } from "../show_specific"; -import { SyncedGraph } from "../SyncedGraph"; -import { Patch } from "../patch"; -import { Literal } from "n3"; -export { Light9Fader } from "./Light9Fader"; - -const log = debug("efffader") - -////////////////////////////////////// -const RETURN_URI = new NamedNode(""); -const RETURN_FLOAT = 1; -function get2Step(returnWhat: T, graph: SyncedGraph, subj1: NamedNode, pred1: NamedNode, pred2: NamedNode): T | undefined { - // ?subj1 ?pred1 ?x . ?x ?pred2 ?returned . - let x: NamedNode; - try { - x = graph.uriValue(subj1, pred1); - } catch (e) { - return undefined; - } - try { - if (typeof returnWhat === "object" && (returnWhat as NamedNode).termType == "NamedNode") { - return graph.uriValue(x, pred2) as T; - } else if (typeof returnWhat === "number") { - return graph.floatValue(x, pred2) as T; - } - } catch (e) { - return undefined; - } -} -function set2Step( - graph: SyncedGraph, // - subj1: NamedNode, - pred1: NamedNode, - baseName: string, - pred2: NamedNode, - newObjLiteral: Literal -) { } - -function maybeUriValue(graph: SyncedGraph, s: NamedNode, p: NamedNode): NamedNode | undefined { - try { - return graph.uriValue(s, p); - } catch (e) { - return undefined; - } -} -function maybeStringValue(graph: SyncedGraph, s: NamedNode, p: NamedNode): string | undefined { - try { - return graph.stringValue(s, p); - } catch (e) { - return undefined; - } -} -function maybeFloatValue(graph: SyncedGraph, s: NamedNode, p: NamedNode): number | undefined { - try { - return graph.floatValue(s, p); - } catch (e) { - return undefined; - } -} - -////////////////////////////////////// -class EffectFader { - constructor(public uri: NamedNode) { } - column: string = "unset"; - effect?: NamedNode; - effectAttr?: NamedNode; // :strength - setting?: NamedNode; // we assume fader always has exactly one setting - value?: number; -} - -@customElement("light9-effect-fader") -export class Light9EffectFader extends LitElement { - static styles = [ - css` - :host { - display: inline-block; - border: 2px gray outset; - background: #272727; - } - light9-fader { - margin: 0px; - width: 100%; - } - `, - ]; - render() { - if (this.conf === undefined || this.conf.value === undefined) { - return html`...`; - } - return html` -
- -
${this.conf.value.toPrecision(3)}
-
effect
-
attr
- `; - } - - graph?: SyncedGraph; - ctx: NamedNode = new NamedNode(showRoot + "/fade"); - @property() uri!: NamedNode; - @state() conf?: EffectFader; // compiled from graph - - constructor() { - super(); - getTopGraph().then((g) => { - this.graph = g; - this.graph.runHandler(this.compile.bind(this, this.graph), `fader config ${this.uri.value}`); - }); - } - - private compile(graph: SyncedGraph) { - const U = graph.U(); - this.conf = undefined; - - const conf = new EffectFader(this.uri); - - if (!graph.contains(this.uri, U("rdf:type"), U(":Fader"))) { - // not loaded yet, perhaps - return; - } - - conf.column = maybeStringValue(graph, this.uri, U(":column")) || "unset"; - conf.effect = maybeUriValue(graph, this.uri, U(":effect")); - conf.effectAttr = get2Step(RETURN_URI, graph, this.uri, U(":setting"), U(":effectAttr")); - - this.conf = conf; - graph.runHandler(this.compileValue.bind(this, graph, this.conf), `fader config.value ${this.uri.value}`); - } - - private compileValue(graph: SyncedGraph, conf: EffectFader) { - // external graph change -> conf.value - const U = graph.U(); - conf.value = get2Step(RETURN_FLOAT, graph, this.uri, U(":setting"), U(":value")); - // since conf attrs aren't watched as property: - this.requestUpdate() - } - - onSliderInput(ev: CustomEvent) { - // slider user input -> graph - if (this.conf === undefined) return; - this.conf.value = ev.detail.value - this.writeValueToGraph() - } - - writeValueToGraph() { - // this.value -> graph - if (this.graph === undefined) { - return; - } - const U = this.graph.U(); - if (this.conf === undefined) { - return; - } - if (this.conf.value === undefined) { - log(`value of ${this.uri} is undefined`) - return; - } - log('writeValueToGraph', this.conf.value) - const valueTerm = this.graph.LiteralRoundedFloat(this.conf.value); - const settingNode = this.graph.uriValue(this.uri, U(":setting")); - this.graph.patchObject(settingNode, this.graph.Uri(":value"), valueTerm, this.ctx); - - } - - onEffectChange(ev: CustomEvent) { - if (this.graph === undefined) { - return; - } - const { newValue } = ev.detail; - this.graph.patchObject(this.uri, this.graph.Uri(":effect"), newValue, this.ctx); - } - - onEffectAttrChange(ev: CustomEvent) { - if (this.graph === undefined) { - return; - } - // const { newValue } = ev.detail; - // if (this.setting === undefined) { - // this.setting = this.graph.nextNumberedResource(this.graph.Uri(":fade_set")); - // this.graph.patchObject(this.uri, this.graph.Uri(":setting"), this.setting, this.ctx); - // } - // this.graph.patchObject(this.setting, this.graph.Uri(":effectAttr"), newValue, this.ctx); - } -} diff -r 623836db99af -r 4556eebe5d73 light9/web/fade/Light9FadeUi.ts --- a/light9/web/fade/Light9FadeUi.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,169 +0,0 @@ -import debug from "debug"; -import { css, html, LitElement, TemplateResult } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import * as N3 from "n3"; -import { NamedNode, Quad } from "n3"; -import { Patch } from "../patch"; -import { getTopGraph } from "../RdfdbSyncedGraph"; -import { showRoot } from "../show_specific"; -import { SyncedGraph } from "../SyncedGraph"; -export { EditChoice } from "../EditChoice"; -export { Light9EffectFader } from "./Light9EffectFader"; -export { Light9Fader } from "./Light9Fader"; - -debug.enable("*,autodep"); -const log = debug("fade"); - -class FaderConfig { - constructor(public uri: NamedNode, public column: number) { } -} - -class FadePage { - constructor(public uri: NamedNode) { } - faderConfigs: FaderConfig[] = []; -} -class FadePages { - pages: FadePage[] = []; -} - -@customElement("light9-fade-ui") -export class Light9FadeUi extends LitElement { - static styles = [ - css` - :host { - display: block; - user-select: none; /* really this is only desirable during slider drag events */ - } - .mappedToHw { - background: #393945; - } - #gm light9-fader { - width: 300px; - } - `, - ]; - render() { - return html` - - -

Fade

-
- grand master -
- ${(this.fadePages?.pages || []).map(this.renderPage.bind(this))} - -
- `; - } - private renderPage(page: FadePage): TemplateResult { - const mappedToHw = this.currentHwPage !== undefined && page.uri.equals(this.currentHwPage); - return html`
-
- - Page - - ${mappedToHw ? html`mapped to hardware sliders` : html` - - `} - - ${page.faderConfigs.map((fd) => html` `)} -
-
`; - } - - graph!: SyncedGraph; - ctx: NamedNode = new NamedNode(showRoot + "/fade"); - - @property() fadePages?: FadePages; - @property() currentHwPage?: NamedNode; - @property() grandMaster?: number; - - constructor() { - super(); - getTopGraph().then((g) => { - this.graph = g; - this.graph.runHandler(this.compile.bind(this), `faders layout`); - this.graph.runHandler(this.compileGm.bind(this), `faders gm`); - }); - } - connectedCallback(): void { - super.connectedCallback(); - } - - compile() { - const U = this.graph.U(); - this.fadePages = undefined; - const fadePages = new FadePages(); - for (let page of this.graph.subjects(U("rdf:type"), U(":FadePage"))) { - const fp = new FadePage(page as NamedNode); - try { - for (let fader of this.graph.objects(page, U(":fader"))) { - const colLit = this.graph.stringValue(fader, U(':column')) - fp.faderConfigs.push(new FaderConfig(fader as NamedNode, parseFloat(colLit))); - } - fp.faderConfigs.sort((a, b) => { - return a.column - (b.column); - }); - fadePages.pages.push(fp); - } catch (e) { } - } - fadePages.pages.sort((a, b) => { - return a.uri.value.localeCompare(b.uri.value); - }); - this.fadePages = fadePages; - this.currentHwPage = undefined; - try { - const mc = this.graph.uriValue(U(":midiControl"), U(":map")); - this.currentHwPage = this.graph.uriValue(mc, U(":outputs")); - } catch (e) { } - } - compileGm() { - const U = this.graph.U(); - this.grandMaster = undefined - let newVal - try { - - newVal = this.graph.floatValue(U(':grandMaster'), U(':value')) - } catch (e) { - return - } - this.grandMaster = newVal; - - } - gmChanged(ev: CustomEvent) { - const U = this.graph.U(); - const newVal = ev.detail.value - // this.grandMaster = newVal; - this.graph.patchObject(U(':grandMaster'), U(':value'), this.graph.LiteralRoundedFloat(newVal), this.ctx) - - } - - - mapThisToHw(page: NamedNode) { - const U = this.graph.U(); - log("map to hw", page); - const mc = this.graph.uriValue(U(":midiControl"), U(":map")); - this.graph.patchObject(mc, U(":outputs"), page, this.ctx); - } - - addPage() { - const U = this.graph.U(); - const uri = this.graph.nextNumberedResource(showRoot + "/fadePage"); - const adds = [ - // - new Quad(uri, U("rdf:type"), U(":FadePage"), this.ctx), - new Quad(uri, U("rdfs:label"), N3.DataFactory.literal("unnamed"), this.ctx), - ]; - for (let n = 1; n <= 8; n++) { - const f = this.graph.nextNumberedResource(showRoot + "/fader"); - const s = this.graph.nextNumberedResource(showRoot + "/faderset"); - adds.push(new Quad(uri, U(":fader"), f, this.ctx)); - adds.push(new Quad(f, U("rdf:type"), U(":Fader"), this.ctx)); - adds.push(new Quad(f, U(":column"), N3.DataFactory.literal("" + n), this.ctx)); - adds.push(new Quad(f, U(":setting"), s, this.ctx)); - adds.push(new Quad(s, U(":effectAttr"), U(":strength"), this.ctx)); - adds.push(new Quad(s, U(":value"), this.graph.LiteralRoundedFloat(0), this.ctx)); - } - this.graph.applyAndSendPatch(new Patch([], adds)); - } -} diff -r 623836db99af -r 4556eebe5d73 light9/web/fade/Light9Fader.ts --- a/light9/web/fade/Light9Fader.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,146 +0,0 @@ -import debug from "debug"; -import { css, html, LitElement, PropertyValueMap } from "lit"; -import { customElement, property, query } from "lit/decorators.js"; - -import { clamp } from "../floating_color_picker"; -const log = debug("fade"); - -class Drag { - constructor(public startDragPxY: number, public startDragValue: number) {} -} - -@customElement("light9-fader") -export class Light9Fader extends LitElement { - static styles = [ - css` - :host { - display: inline-block; - border: 2px gray inset; - background: #000; - height: 80px; - } - #handle { - background: gray; - border: 5px gray outset; - position: relative; - left: 0; - right: -25px; - } - `, - ]; - - @property() value: number = 0; - - @query("#handle") handleEl!: HTMLElement; - - troughHeight = 80 - 2 - 2 - 5 - 5; - handleHeight = 10; - - drag?: Drag; - unmutedValue: number = 1; - - render() { - return html`

`; - } - - protected update(changedProperties: PropertyValueMap | Map): void { - super.update(changedProperties); - if (changedProperties.has("value")) { - - } - } - valueChangedFromUi() { - this.value= clamp(this.value, 0, 1) - this.dispatchEvent(new CustomEvent("change", { detail: { value: this.value } })); - } - - protected updated(_changedProperties: PropertyValueMap | Map): void { - super.updated(_changedProperties); - const y = this.sliderTopY(this.value); - this.handleEl.style.top = y + "px"; - } - - protected firstUpdated(_changedProperties: PropertyValueMap | Map): void { - super.firstUpdated(_changedProperties); - this.handleEl.style.height = this.handleHeight + "px"; - this.events(); - } - - events() { - const hand = this.handleEl; - hand.addEventListener("mousedown", (ev: MouseEvent) => { - ev.stopPropagation(); - if (ev.buttons == 1) { - this.drag = new Drag(ev.clientY, this.value); - } else if (ev.buttons == 2) { - this.onRmb(); - } - }); - this.addEventListener("mousedown", (ev: MouseEvent) => { - ev.stopPropagation(); - if (ev.buttons == 1) { - this.value = this.sliderValue(ev.offsetY); - this.valueChangedFromUi() - this.drag = new Drag(ev.clientY, this.value); - } else if (ev.buttons == 2) { - // RMB in trough - this.onRmb(); - } - }); - - this.addEventListener("contextmenu", (event) => { - event.preventDefault(); - }); - - this.addEventListener("wheel", (ev: WheelEvent) => { - ev.preventDefault(); - this.value += ev.deltaY / this.troughHeight * -.05; - this.valueChangedFromUi() - }); - - const maybeDrag = (ev: MouseEvent) => { - if (ev.buttons != 1) return; - if (this.drag === undefined) return; - ev.stopPropagation(); - this.onMouseDrag(ev.clientY - this.drag.startDragPxY!); - }; - hand.addEventListener("mousemove", maybeDrag); - this.addEventListener("mousemove", maybeDrag); - window.addEventListener("mousemove", maybeDrag); - - hand.addEventListener("mouseup", this.onMouseUpAnywhere.bind(this)); - this.addEventListener("mouseup", this.onMouseUpAnywhere.bind(this)); - window.addEventListener("mouseup", this.onMouseUpAnywhere.bind(this)); - } - onRmb() { - if (this.value > 0.1) { - // mute - this.unmutedValue = this.value; - this.value = 0; - } else { - // unmute - this.value = this.unmutedValue; - } - this.valueChangedFromUi() - } - onMouseDrag(dy: number) { - if (this.drag === undefined) throw "unexpected"; - this.value = this.drag.startDragValue - dy / this.troughHeight; - this.valueChangedFromUi() - } - - onMouseUpAnywhere() { - this.drag = undefined; - } - - sliderTopY(value: number): number { - const usableY = this.troughHeight - this.handleHeight; - const yAdj = this.handleHeight / 2 - 5 - 2; - return (1 - value) * usableY + yAdj; - } - sliderValue(offsetY: number): number { - const usableY = this.troughHeight - this.handleHeight; - const yAdj = this.handleHeight / 2 - 5 - 2; - return clamp(1 - (offsetY - yAdj) / usableY, 0, 1); - } -} diff -r 623836db99af -r 4556eebe5d73 light9/web/fade/index.html --- a/light9/web/fade/index.html Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,13 +0,0 @@ - - - - fade - - - - - - - - - diff -r 623836db99af -r 4556eebe5d73 light9/web/floating_color_picker.ts --- a/light9/web/floating_color_picker.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,321 +0,0 @@ -// Note that this file deals only with hue+sat. See Light9ColorPicker for the value component. - -import debug from "debug"; -import { css, html, LitElement } from "lit"; -import { customElement, query } from "lit/decorators.js"; -import color from "onecolor"; -import { SubEvent } from "sub-events"; - -const log = debug("control.color.pick"); - -export function clamp(x: number, lo: number, hi: number) { - return Math.max(lo, Math.min(hi, x)); -} - -class RainbowCoord { - // origin is rainbow top-lefft - constructor(public x: number, public y: number) {} -} - -export class ClientCoord { - // origin is top-left of client viewport (regardless of page scroll) - constructor(public x: number, public y: number) {} -} - -// Load the rainbow, and map between colors and pixels. -class RainbowCanvas { - ctx: CanvasRenderingContext2D; - colorPos: { [color: string]: RainbowCoord } = {}; - _loaded = false; - _loadWatchers: (() => void)[] = []; - constructor(url: string, public size: RainbowCoord) { - var elem = document.createElement("canvas"); - elem.width = size.x; - elem.height = size.y; - this.ctx = elem.getContext("2d")!; - - var img = new Image(); - img.onload = () => { - this.ctx.drawImage(img, 0, 0); - this._readImage(); - this._loaded = true; - this._loadWatchers.forEach(function (cb) { - cb(); - }); - this._loadWatchers = []; - }; - img.src = url; - } - - onLoad(cb: () => void) { - // we'll call this when posFor is available - if (this._loaded) { - cb(); - return; - } - this._loadWatchers.push(cb); - } - - _readImage() { - var data = this.ctx.getImageData(0, 0, this.size.x, this.size.y).data; - for (var y = 0; y < this.size.y; y += 1) { - for (var x = 0; x < this.size.x; x += 1) { - var base = (y * this.size.x + x) * 4; - let px = [data[base + 0], data[base + 1], data[base + 2], 255]; - if (px[0] == 0 && px[1] == 0 && px[2] == 0) { - // (there's no black on the rainbow images) - throw new Error(`color picker canvas (${this.size.x}) returns 0,0,0`); - } - var c = color(px).hex(); - this.colorPos[c] = new RainbowCoord(x, y); - } - } - } - - colorAt(pos: RainbowCoord) { - var data = this.ctx.getImageData(pos.x, pos.y, 1, 1).data; - return color([data[0], data[1], data[2], 255]).hex(); - } - - posFor(col: string): RainbowCoord { - if (col == "#000000") { - throw new Error("no match"); - } - - log("col", col); - if (col == "#ffffff") { - return new RainbowCoord(400 / 2, 0); - } - - let bright = color(col).value(1).hex(); - let r = parseInt(bright.slice(1, 3), 16), - g = parseInt(bright.slice(3, 5), 16), - b = parseInt(bright.slice(5, 7), 16); - - // We may not have a match for this color exactly (e.g. on - // the small image), so we have to search for a near one. - - // 0, 1, -1, 2, -2, ... - let walk = function (x: number): number { - return -x + (x > 0 ? 0 : 1); - }; - - var radius = 8; - for (var dr = 0; dr < radius; dr = walk(dr)) { - for (var dg = 0; dg < radius; dg = walk(dg)) { - for (var db = 0; db < radius; db = walk(db)) { - // Don't need bounds check- out of range - // corrupt colors just won't match. - const color2 = color([r + dr, g + dg, b + db, 255]); - const pos = this.colorPos[color2.hex()]; - if (pos !== undefined) { - return pos; - } - } - } - } - throw new Error("no match"); - } -} - -// One-per-page element that floats above everything. Plus the scrim element, which is also per-page. -@customElement("light9-color-picker-float") -class Light9ColorPickerFloat extends LitElement { - static styles = [ - css` - :host { - z-index: 10; - position: fixed; /* host coords are the same as client coords /* - left: 0; - top: 0; - width: 100%; - height: 100%; - - /* Updated later. */ - display: none; - } - #largeCrosshair { - position: absolute; - left: -60px; - top: -62px; - pointer-events: none; - } - #largeCrosshair { - background: url(/colorpick_crosshair_large.svg); - width: 1000px; - height: 1000px; - } - #largeRainbowComp { - z-index: 2; - position: relative; - width: 400px; - height: 200px; - border: 10px solid #000; - box-shadow: 8px 11px 40px 0px rgba(0, 0, 0, 0.74); - overflow: hidden; - } - #largeRainbow { - background: url(/colorpick_rainbow_large.png); - width: 400px; - height: 200px; - user-select: none; - } - #outOfBounds { - user-select: none; - z-index: 1; - background: #00000060; - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - } - `, - ]; - - @query("#largeCrosshair") largeCrosshairEl!: HTMLElement; - @query("#largeRainbow") largeRainbowEl!: HTMLElement; - - canvasMove: SubEvent = new SubEvent(); - outsideMove: SubEvent = new SubEvent(); - mouseUp: SubEvent = new SubEvent(); - - render() { - return html` - -
-
-
-
-
- `; - } - - // make top-left of rainbow image be at this pos - placeRainbow(pos: ClientCoord) { - const el = this.shadowRoot?.querySelector("#largeRainbowComp")! as HTMLElement; - const cssBorder = 10; - el.style.left = pos.x - cssBorder + "px"; - el.style.top = pos.y - cssBorder + "px"; - } - - moveLargeCrosshair(pos: RainbowCoord) { - const ch = this.largeCrosshairEl; - ch.style.left = pos.x - ch.offsetWidth / 2 + "px"; - ch.style.top = pos.y - ch.offsetHeight / 2 + "px"; - } - - private onCanvasMove(ev: MouseEvent) { - this.canvasMove.emit(new RainbowCoord(ev.offsetX, ev.offsetY)); - } - - private onMouseUp(ev: MouseEvent) { - this.mouseUp.emit(); - } - - private onOutOfBoundsMove(ev: MouseEvent) { - this.outsideMove.emit(new ClientCoord(ev.clientX, ev.clientY)); - } -} - -class PickerFloat { - private rainbow?: RainbowCanvas; - private currentListener?: (hsc: string) => void; - private rainbowOrigin: ClientCoord = new ClientCoord(0, 0); - private floatEl?: Light9ColorPickerFloat; - - pageInit() { - this.getFloatEl(); - this.getRainbow(); - } - private forceHostStyle(el: HTMLElement) { - el.style.zIndex = "10"; - el.style.position = "fixed"; - el.style.left = "0"; - el.style.top = "0"; - el.style.width = "100%"; - el.style.height = "100%"; - el.style.display = "none"; - } - private getFloatEl(): Light9ColorPickerFloat { - if (!this.floatEl) { - this.floatEl = document.createElement("light9-color-picker-float") as Light9ColorPickerFloat; - this.forceHostStyle(this.floatEl); - this.subscribeToFloatElement(this.floatEl); - document.body.appendChild(this.floatEl); - } - return this.floatEl; - } - - private subscribeToFloatElement(el: Light9ColorPickerFloat) { - el.canvasMove.subscribe(this.onCanvasMove.bind(this)); - el.outsideMove.subscribe(this.onOutsideMove.bind(this)); - el.mouseUp.subscribe(() => { - this.hide(); - }); - } - - private onCanvasMove(pos: RainbowCoord) { - pos = new RainbowCoord( // - clamp(pos.x, 0, 400 - 1), // - clamp(pos.y, 0, 200 - 1) - ); - this.getFloatEl().moveLargeCrosshair(pos); - if (this.currentListener) { - this.currentListener(this.getRainbow().colorAt(pos)); - } - } - - private onOutsideMove(pos: ClientCoord) { - const rp = this.toRainbow(pos); - this.onCanvasMove(rp); - } - - private getRainbow(): RainbowCanvas { - if (!this.rainbow) { - this.rainbow = new RainbowCanvas("/colorpick_rainbow_large.png", new RainbowCoord(400, 200)); - } - return this.rainbow; - } - - startPick(clickPoint: ClientCoord, startColor: string, onNewHueSatColor: (hsc: string) => void) { - const el = this.getFloatEl(); - - let pos: RainbowCoord; - try { - pos = this.getRainbow().posFor(startColor); - } catch (e) { - pos = new RainbowCoord(-999, -999); - } - - this.rainbowOrigin = new ClientCoord( // - clickPoint.x - clamp(pos.x, 0, 400), // - clickPoint.y - clamp(pos.y, 0, 200) - ); - - el.placeRainbow(this.rainbowOrigin); - setTimeout(() => { - this.getFloatEl().moveLargeCrosshair(pos); - }, 1); - - el.style.display = "block"; - this.currentListener = onNewHueSatColor; - } - - private hide() { - const el = this.getFloatEl(); - el.style.display = "none"; - this.currentListener = undefined; - } - - private toRainbow(pos: ClientCoord): RainbowCoord { - return new RainbowCoord( // - pos.x - this.rainbowOrigin.x, // - pos.y - this.rainbowOrigin.y - ); - } -} - -export const pickerFloat = new PickerFloat(); diff -r 623836db99af -r 4556eebe5d73 light9/web/graph_test.coffee --- a/light9/web/graph_test.coffee Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,257 +0,0 @@ -log = console.log -assert = require('chai').assert -expect = require('chai').expect -SyncedGraph = require('./graph.js').SyncedGraph - -describe 'SyncedGraph', -> - describe 'constructor', -> - it 'should successfully make an empty graph without connecting to rdfdb', -> - g = new SyncedGraph() - g.quads() - assert.equal(g.quads().length, 0) - - describe 'auto dependencies', -> - graph = new SyncedGraph() - RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' - U = (tail) -> graph.Uri('http://example.com/' + tail) - A1 = U('a1') - A2 = U('a2') - A3 = U('a3') - A4 = U('a4') - ctx = U('ctx') - quad = (s, p, o) -> graph.Quad(s, p, o, ctx) - - beforeEach (done) -> - graph = new SyncedGraph() - graph.loadTrig(" - @prefix : . - :ctx { - :a1 :a2 :a3 . - :a1 :someFloat 1.5 . - :a1 :someString \"hello\" . - :a1 :multipleObjects :a4, :a5 . - :a2 a :Type1 . - :a3 a :Type1 . - } - ", done) - - it 'calls a handler right away', -> - called = 0 - hand = -> - called++ - graph.runHandler(hand, 'run') - assert.equal(1, called) - - it 'calls a handler a 2nd time if the graph is patched with relevant data', -> - called = 0 - hand = -> - called++ - graph.uriValue(A1, A2) - graph.runHandler(hand, 'run') - graph.applyAndSendPatch({ - delQuads: [quad(A1, A2, A3)], addQuads: [quad(A1, A2, A4)]}) - assert.equal(2, called) - - it 'notices new queries a handler makes upon rerun', -> - called = 0 - objsFound = [] - hand = -> - called++ - graph.uriValue(A1, A2) - if called > 1 - objsFound.push(graph.objects(A1, A3)) - graph.runHandler(hand, 'run') - # first run looked up A1,A2,* - graph.applyAndSendPatch({ - delQuads: [quad(A1, A2, A3)], addQuads: [quad(A1, A2, A4)]}) - # second run also looked up A1,A3,* (which matched none) - graph.applyAndSendPatch({ - delQuads: [], addQuads: [quad(A1, A3, A4)]}) - # third run should happen here, noticing the new A1,A3,* match - assert.equal(3, called) - assert.deepEqual([[], [A4]], objsFound) - - it 'calls a handler again even if the handler throws an error', -> - called = 0 - hand = -> - called++ - graph.uriValue(A1, A2) - throw new Error('this test handler throws an error') - graph.runHandler(hand, 'run') - graph.applyAndSendPatch({ - delQuads: [quad(A1, A2, A3)], addQuads: [quad(A1, A2, A4)]}) - assert.equal(2, called) - - describe 'works with nested handlers', -> - - innerResults = [] - inner = -> - console.log('\nninnerfetch') - innerResults.push(graph.uriValue(A1, A2)) - console.log("innerResults #{JSON.stringify(innerResults)}\n") - - outerResults = [] - doRunInner = true - outer = -> - if doRunInner - graph.runHandler(inner, 'runinner') - console.log('push outer') - outerResults.push(graph.floatValue(A1, U('someFloat'))) - - beforeEach -> - innerResults = [] - outerResults = [] - doRunInner = true - - affectInner = { - delQuads: [quad(A1, A2, A3)], addQuads: [quad(A1, A2, A4)] - } - affectOuter = { - delQuads: [ - quad(A1, U('someFloat'), graph.Literal(1.5)) - ], addQuads: [ - quad(A1, U('someFloat'), graph.LiteralRoundedFloat(2)) - ]} - affectBoth = { - delQuads: affectInner.delQuads.concat(affectOuter.delQuads), - addQuads: affectInner.addQuads.concat(affectOuter.addQuads) - } - - it 'calls everything normally once', -> - graph.runHandler(outer, 'run') - assert.deepEqual([A3], innerResults) - assert.deepEqual([1.5], outerResults) - - it.skip '[performance] reruns just the inner if its dependencies change', -> - console.log(graph.quads()) - graph.runHandler(outer, 'run') - graph.applyAndSendPatch(affectInner) - assert.deepEqual([A3, A4], innerResults) - assert.deepEqual([1.5], outerResults) - - it.skip '[performance] reruns the outer (and therefore inner) if its dependencies change', -> - graph.runHandler(outer, 'run') - graph.applyAndSendPatch(affectOuter) - assert.deepEqual([A3, A3], innerResults) - assert.deepEqual([1.5, 2], outerResults) - - - it.skip '[performance] does not send a redundant inner run if it is already rerunning outer', -> - # Note that outer may or may not call inner each time, and we - # don't want to redundantly call inner. We need to: - # 1. build the set of handlers to rerun, - # 2. call them from outside-in, and - # 3. any runHandler calls that happen, they need to count as reruns. - graph.runHandler(outer, 'run') - graph.applyAndSendPatch(affectBoth) - assert.deepEqual([A3, A4], innerResults) - assert.deepEqual([1.5, 2], outerResults) - - it 'reruns the outer and the inner if all dependencies change, but outer omits calling inner this time', -> - graph.runHandler(outer, 'run') - doRunInner = false - graph.applyAndSendPatch(affectBoth) - assert.deepEqual([A3, A4], innerResults) - assert.deepEqual([1.5, 2], outerResults) - - describe 'watches calls to:', -> - it 'floatValue', -> - values = [] - hand = -> values.push(graph.floatValue(A1, U('someFloat'))) - graph.runHandler(hand, 'run') - graph.patchObject(A1, U('someFloat'), graph.LiteralRoundedFloat(2), ctx) - assert.deepEqual([1.5, 2.0], values) - - it 'stringValue', -> - values = [] - hand = -> values.push(graph.stringValue(A1, U('someString'))) - graph.runHandler(hand, 'run') - graph.patchObject(A1, U('someString'), graph.Literal('world'), ctx) - assert.deepEqual(['hello', 'world'], values) - - it 'uriValue', -> - # covered above, but this one tests patchObject on a uri, too - values = [] - hand = -> values.push(graph.uriValue(A1, A2)) - graph.runHandler(hand, 'run') - graph.patchObject(A1, A2, A4, ctx) - assert.deepEqual([A3, A4], values) - - it 'objects', -> - values = [] - hand = -> values.push(graph.objects(A1, U('multipleObjects'))) - graph.runHandler(hand, 'run') - graph.patchObject(A1, U('multipleObjects'), U('newOne'), ctx) - expect(values[0]).to.deep.have.members([U('a4'), U('a5')]) - expect(values[1]).to.deep.have.members([U('newOne')]) - - it 'subjects', -> - values = [] - rdfType = graph.Uri(RDF + 'type') - hand = -> values.push(graph.subjects(rdfType, U('Type1'))) - graph.runHandler(hand, 'run') - graph.applyAndSendPatch( - {delQuads: [], addQuads: [quad(A4, rdfType, U('Type1'))]}) - expect(values[0]).to.deep.have.members([A2, A3]) - expect(values[1]).to.deep.have.members([A2, A3, A4]) - - describe 'items', -> - it 'when the list order changes', (done) -> - values = [] - successes = 0 - hand = -> - try - head = graph.uriValue(U('x'), U('y')) - catch - # graph goes empty between clearGraph and loadTrig - return - values.push(graph.items(head)) - successes++ - graph.clearGraph() - graph.loadTrig " - @prefix : . - :ctx { :x :y (:a1 :a2 :a3) } . - ", () -> - graph.runHandler(hand, 'run') - graph.clearGraph() - graph.loadTrig " - @prefix : . - :ctx { :x :y (:a1 :a3 :a2) } . - ", () -> - assert.deepEqual([[A1, A2, A3], [A1, A3, A2]], values) - assert.equal(2, successes) - done() - - describe 'contains', -> - it 'when a new triple is added', -> - values = [] - hand = -> values.push(graph.contains(A1, A1, A1)) - graph.runHandler(hand, 'run') - graph.applyAndSendPatch( - {delQuads: [], addQuads: [quad(A1, A1, A1)]}) - assert.deepEqual([false, true], values) - - it 'when a relevant triple is removed', -> - values = [] - hand = -> values.push(graph.contains(A1, A2, A3)) - graph.runHandler(hand, 'run') - graph.applyAndSendPatch( - {delQuads: [quad(A1, A2, A3)], addQuads: []}) - assert.deepEqual([true, false], values) - - describe 'performs well', -> - it "[performance] doesn't call handler a 2nd time if the graph gets an unrelated patch", -> - called = 0 - hand = -> - called++ - graph.uriValue(A1, A2) - graph.runHandler(hand, 'run') - graph.applyAndSendPatch({ - delQuads: [], addQuads: [quad(A2, A3, A4)]}) - assert.equal(1, called) - - it.skip '[performance] calls a handler 2x but then not again if the handler stopped caring about the data', -> - assert.fail() - - it.skip "[performance] doesn't get slow if the handler makes tons of repetitive lookups", -> - assert.fail() diff -r 623836db99af -r 4556eebe5d73 light9/web/index.html --- a/light9/web/index.html Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,13 +0,0 @@ - - - - light9 home - - - - - - -
- - diff -r 623836db99af -r 4556eebe5d73 light9/web/lib/.bowerrc --- a/light9/web/lib/.bowerrc Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ -{ - "directory": "." -} - diff -r 623836db99af -r 4556eebe5d73 light9/web/lib/bower.json --- a/light9/web/lib/bower.json Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,46 +0,0 @@ -{ - "name": "3rd-party-libs", - "dependencies": { - "N3.js": "https://github.com/RubenVerborgh/N3.js.git#04f4e21f4ccb351587dc00a3f26340b28d4bb10f", - "QueryString": "http://unixpapa.com/js/QueryString.js", - "async": "https://github.com/caolan/async.git#^1.5.2", - "color": "https://github.com/One-com/one-color.git#^3.0.4", - "iron-ajax": "PolymerElements/iron-ajax#~2.1.3", - "iron-resizable-behavior": "PolymerElements/iron-resizable-behavior#^2.1.0", - "isotope": "^3.0.4", - "isotope-fit-columns": "^1.1.3", - "jquery": "^3.3.1", - "jquery-ui": "~1.11.4", - "jquery.columnizer": "https://github.com/adamwulf/Columnizer-jQuery-Plugin.git#^1.6.2", - "knockout": "knockoutjs#^3.4.2", - "paper-button": "PolymerElements/paper-button#^2.1.1", - "paper-dialog": "PolymerElements/paper-dialog#^2.1.0", - "paper-item": "PolymerElements/paper-item#2.1.0", - "paper-listbox": "PolymerElements/paper-listbox#2.1.0", - "paper-radio-button": "PolymerElements/paper-radio-button#^2.1.0", - "paper-radio-group": "PolymerElements/paper-radio-group#^2.1.0", - "paper-slider": "PolymerElements/paper-slider#~2.0.6", - "paper-styles": "PolymerElements/paper-styles#^2.1.0", - "rdflib.js": "https://github.com/linkeddata/rdflib.js.git#920e59fe37", - "rdfstore": "https://github.com/antoniogarrote/rdfstore-js.git#b3f7c0c9c1da9b26261af0d4858722fa982411bb", - "shortcut": "http://www.openjs.com/scripts/events/keyboard_shortcuts/shortcut.js", - "sylvester": "~0.1.3", - "underscore": "~1.8.3", - "polymer": "Polymer/polymer#^2.0.0", - "iron-flex-layout": "PolymerElements/iron-flex-layout#^2.0.3", - "iron-component-page": "PolymerElements/iron-component-page#^3.0.1", - "paper-header-panel": "PolymerElements/paper-header-panel#^2.1.0", - "iron-overlay-behavior": "PolymerElements/iron-overlay-behavior#^2.3.4", - "debug": "https://github.com/visionmedia/debug/archive/master.zip" - }, - "resolutions": { - "webcomponentsjs": "^v1.1.0", - "polymer": "^2.0.0", - "iron-flex-layout": "^2.0.3", - "paper-button": "^2.1.1", - "iron-component-page": "^3.0.1", - "iron-doc-viewer": "^3.0.0", - "paper-header-panel": "^2.1.0", - "iron-overlay-behavior": "^2.3.4" - } -} diff -r 623836db99af -r 4556eebe5d73 light9/web/lib/onecolor.d.ts --- a/light9/web/lib/onecolor.d.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,13 +0,0 @@ -type ColorFormat = "hex" | "rgb" | "hsl" | "hsv"; -interface Color { - clone(): this; - toString(format?: ColorFormat): string; - toJSON(): string; - value(): number; - value(v: number): this; - hex(): string; -} - -declare function color(value: any): Color; - -export = color; diff -r 623836db99af -r 4556eebe5d73 light9/web/lib/parse-prometheus-text-format.d.ts --- a/light9/web/lib/parse-prometheus-text-format.d.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ -declare module "parse-prometheus-text-format" { - function parsePrometheusTextFormat(s: string): any; - export default parsePrometheusTextFormat; -} diff -r 623836db99af -r 4556eebe5d73 light9/web/lib/sylvester.d.ts --- a/light9/web/lib/sylvester.d.ts Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,828 +0,0 @@ -// local fixes; the DefinitelyTyped one had "is not a module" errors - - -// Type definitions for sylvester 0.1.3 -// Project: https://github.com/jcoglan/sylvester -// Definitions by: Stephane Alie -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped - -// === Sylvester === -// Vector and Matrix mathematics modules for JavaScript -// Copyright (c) 2007 James Coglan - -export declare module Sylvester { - interface VectorStatic { - /** - * Constructor function. - */ - create(elements: Vector|Array): Vector; - - i: Vector; - j: Vector; - k: Vector; - - /** - * Random vector of size n. - * - * @param {number} n The vector size. - */ - Random(n: number): Vector; - - /** - * Vector filled with zeros. - * - * @param {number} n The vector size. - */ - Zero(n: number): Vector; - } - interface MatrixStatic { - /** - * Constructor function. - * - * @param {Array|Array>|Vector|Matrix} elements The elements. - */ - create(elements: Array|Array>|Vector | Matrix): Matrix; - - /** - * Identity matrix of size n. - * - * @param {number} n The size. - */ - I(n: number): Matrix; - - /** - * Diagonal matrix - all off-diagonal elements are zero - * - * @param {any} elements The elements. - */ - Diagonal(elements: Array|Array>|Vector | Matrix): Matrix; - - /** - * Rotation matrix about some axis. If no axis is supplied, assume we're after a 2D transform. - * - * @param {number} theta The angle in radians. - * @param {Vector} a [Optional] The axis. - */ - Rotation(theta: number, a?: Vector): Matrix; - - RotationX(t: number): Matrix; - RotationY(t: number): Matrix; - RotationZ(t: number): Matrix; - - /** - * Random matrix of n rows, m columns. - * - * @param {number} n The number of rows. - * @param {number} m The number of columns. - */ - Random(n: number, m: number): Matrix; - - /** - * Matrix filled with zeros. - * - * @param {number} n The number of rows. - * @param {number} m The number of columns. - */ - Zero(n: number, m: number): Matrix; - } - - interface LineStatic { - /** - * Constructor function. - * - * @param Array|Vector anchor The anchor vector. - * @param Array|Vector direction The direction vector. - */ - create(anchor: Array|Vector, direction: Array|Vector): Line; - - X: Line; - Y: Line; - Z: Line; - } - interface PlaneStatic { - /** - * Constructor function. - */ - create(anchor: Array|Vector, normal: Array|Vector): Plane; - - /** - * Constructor function. - */ - create(anchor: Array|Vector, v1: Array|Vector, v2: Array|Vector): Plane; - - XY: Plane; - YZ: Plane; - ZX: Plane; - YX: Plane; - } -} - -interface Vector { - /** - * Gets an array containing the vector's elements. - */ - elements: Array; - - /** - * Returns element i of the vector. - */ - e(i: number): number; - - /** - * Returns the number of elements the vector has. - */ - dimensions(): number; - - /** - * Returns the modulus ('length') of the vector. - */ - modulus(): number; - - /** - * Returns true if the vector is equal to the argument. - * - * @param {Vector|Array} vector The vector to compare equality. - */ - eql(vector: Vector|Array): boolean; - - /** - * Returns a copy of the vector. - */ - dup(): Vector; - - /** - * Maps the vector to another vector according to the given function. - * - * @param {Function} fn The function to apply to each element (x, i) => {}. - */ - map(fn: (x: number, i: number) => any): Vector; - - /** - * Calls the iterator for each element of the vector in turn. - * - * @param {Function} fn The function to apply to each element (x, i) => {}. - */ - each(fn: (x: number, i: number) => any): void; - - /** - * Returns a new vector created by normalizing the receiver. - */ - toUnitVector(): Vector; - - /** - * Returns the angle between the vector and the argument (also a vector). - * - * @param {Vector} vector The other vector to calculate the angle. - */ - angleFrom(vector: Vector): number; - - /** - * Returns true if the vector is parallel to the argument. - * - * @param {Vector} vector The other vector. - */ - isParallelTo(vector: Vector): boolean; - - /** - * Returns true if the vector is antiparallel to the argument. - * - * @param {Vector} vector The other vector. - */ - isAntiparallelTo(vector: Vector): boolean; - - /** - * Returns true iff the vector is perpendicular to the argument. - * - * @param {Vector} vector The other vector. - */ - isPerpendicularTo(vector: Vector): boolean; - - /** - * Returns the result of adding the argument to the vector. - * - * @param {Vector|Array} vector The vector. - */ - add(vector: Vector|Array): Vector; - - /** - * Returns the result of subtracting the argument from the vector. - * - * @param {Vector|Array} vector The vector. - */ - subtract(vector: Vector|Array): Vector; - - /** - * Returns the result of multiplying the elements of the vector by the argument. - * - * @param {number} k The value by which to multiply the vector. - */ - multiply(k: number): Vector; - - /** - * Returns the result of multiplying the elements of the vector by the argument (Alias for multiply(k)). - * - * @param {number} k The value by which to multiply the vector. - */ - x(k: number): Vector; - - /** - * Returns the scalar product of the vector with the argument. Both vectors must have equal dimensionality. - * - * @param: {Vector|Array} vector The other vector. - */ - dot(vector: Vector|Array): number; - - /** - * Returns the vector product of the vector with the argument. Both vectors must have dimensionality 3. - * - * @param {Vector|Array} vector The other vector. - */ - cross(vector: Vector|Array): Vector; - - /** - * Returns the (absolute) largest element of the vector. - */ - max(): number; - - /** - * Returns the index of the first match found. - * - * @param {number} x The value. - */ - indexOf(x: number): number; - - /** - * Returns a diagonal matrix with the vector's elements as its diagonal elements. - */ - toDiagonalMatrix(): Matrix; - - /** - * Returns the result of rounding the elements of the vector. - */ - round(): Vector; - - /** - * Returns a copy of the vector with elements set to the given value if they differ from - * it by less than Sylvester.precision. - * - * @param {number} x The value to snap to. - */ - snapTo(x: number): Vector; - - /** - * Returns the vector's distance from the argument, when considered as a point in space. - * - * @param {Vector|Line|Plane} obj The object to calculate the distance. - */ - distanceFrom(obj: Vector|Line|Plane): number; - - /** - * Returns true if the vector is point on the given line. - * - * @param {Line} line The line. - */ - liesOn(line: Line): boolean; - - /** - * Return true if the vector is a point in the given plane. - * - * @param {Plane} plane The plane. - */ - liesIn(plane: Plane): boolean; - - /** - * Rotates the vector about the given object. The object should be a point if the vector is 2D, - * and a line if it is 3D. Be careful with line directions! - * - * @param {number|Matrix} t The angle in radians or in rotation matrix. - * @param {Vector|Line} obj The rotation axis. - */ - rotate(t: number|Matrix, obj: Vector|Line): Vector; - - /** - * Returns the result of reflecting the point in the given point, line or plane. - * - * @param {Vector|Line|Plane} obj The object. - */ - reflectionIn(obj: Vector|Line|Plane): Vector; - - /** - * Utility to make sure vectors are 3D. If they are 2D, a zero z-component is added. - */ - to3D(): Vector; - - /** - * Returns a string representation of the vector. - */ - inspect(): string; - - /** - * Set vector's elements from an array. - * - * @param {Vector|Array} els The elements. - */ - setElements(els: Vector|Array): Vector; -} - -interface Matrix { - /** - * Gets a nested array containing the matrix's elements. - */ - elements: Array>; - /** - * Returns element (i,j) of the matrix. - * - * @param {number} i The row index. - * @param {number} j The column index. - */ - e(i: number, j: number): any; - - /** - * Returns row k of the matrix as a vector. - * - * @param {number} i The row index. - */ - row(i: number): Vector; - - /** - * Returns column k of the matrix as a vector. - * - * @param {number} j The column index. - */ - col(j: number): Vector; - - /** - * Returns the number of rows/columns the matrix has. - * - * @return {any} An object { rows: , cols: }. - */ - dimensions(): any; - - /** - * Returns the number of rows in the matrix. - */ - rows(): number; - - /** - * Returns the number of columns in the matrix. - */ - cols(): number; - - /** - * Returns true if the matrix is equal to the argument. You can supply a vector as the argument, - * in which case the receiver must be a one-column matrix equal to the vector. - * - * @param {Vector|Matrix} matrix The argument to compare. - */ - eql(matrix: Vector|Matrix): boolean; - - /** - * Returns a copy of the matrix. - */ - dup(): Matrix; - - /** - * Maps the matrix to another matrix (of the same dimensions) according to the given function. - * - * @param {Function} fn The function. - */ - map(fn: (x: number, i: number, j: number) => any): Matrix; - - /** - * Returns true iff the argument has the same dimensions as the matrix. - * - * @param {Matrix} matrix The other matrix. - */ - isSameSizeAs(matrix: Matrix): boolean; - - /** - * Returns the result of adding the argument to the matrix. - * - * @param {Matrix} matrix The matrix to add. - */ - add(matrix: Matrix): Matrix; - - /** - * Returns the result of subtracting the argument from the matrix. - * - * @param {Matrix} matrix The matrix to substract. - */ - subtract(matrix: Matrix): Matrix; - - /** - * Returns true iff the matrix can multiply the argument from the left. - * - * @param {Matrix} matrix The matrix. - */ - canMultiplyFromLeft(matrix: Matrix): boolean; - - /** - * Returns the result of multiplying the matrix from the right by the argument. If the argument is a scalar - * then just multiply all the elements. If the argument is a vector, a vector is returned, which saves you - * having to remember calling col(1) on the result. - * - * @param {number|Matrix} matrix The multiplier. - */ - multiply(matrix: number|Matrix): Matrix; - - /** - * Returns the result of multiplying the matrix from the right by the argument. If the argument is a scalar - * then just multiply all the elements. If the argument is a vector, a vector is returned, which saves you - * having to remember calling col(1) on the result. - * - * @param {Vector} vector The multiplier. - */ - multiply(vector: Vector): Vector; - - x(matrix: number|Matrix): Matrix; - - x(vector: Vector): Vector; - - /** - * Returns a submatrix taken from the matrix. Argument order is: start row, start col, nrows, ncols. - * Element selection wraps if the required index is outside the matrix's bounds, so you could use - * this to perform row/column cycling or copy-augmenting. - * - * @param {number} a Starting row index. - * @param {number} b Starting column index. - * @param {number} c Number of rows. - * @param {number} d Number of columns. - */ - minor(a: number, b: number, c: number, d: number): Matrix; - - /** - * Returns the transpose of the matrix. - */ - transpose(): Matrix; - - /** - * Returns true if the matrix is square. - */ - isSquare(): boolean; - - /** - * Returns the (absolute) largest element of the matrix. - */ - max(): number; - - /** - * Returns the indeces of the first match found by reading row-by-row from left to right. - * - * @param {number} x The value. - * - * @return {any} The element indeces i.e: { row:1, col:1 } - */ - indexOf(x: number): any; - - /** - * If the matrix is square, returns the diagonal elements as a vector; otherwise, returns null. - */ - diagonal(): Vector; - - /** - * Make the matrix upper (right) triangular by Gaussian elimination. This method only adds multiples - * of rows to other rows. No rows are scaled up or switched, and the determinant is preserved. - */ - toRightTriangular(): Matrix; - toUpperTriangular(): Matrix; - - /** - * Returns the determinant for square matrices. - */ - determinant(): number; - det(): number; - - /** - * Returns true if the matrix is singular. - */ - isSingular(): boolean; - - /** - * Returns the trace for square matrices. - */ - trace(): number; - tr(): number; - - /** - * Returns the rank of the matrix. - */ - rank(): number; - rk(): number; - - /** - * Returns the result of attaching the given argument to the right-hand side of the matrix. - * - * @param {Matrix|Vector} matrix The matrix or vector. - */ - augment(matrix: Matrix|Vector): Matrix; - - /** - * Returns the inverse (if one exists) using Gauss-Jordan. - */ - inverse(): Matrix; - inv(): Matrix; - - /** - * Returns the result of rounding all the elements. - */ - round(): Matrix; - - /** - * Returns a copy of the matrix with elements set to the given value if they differ from it - * by less than Sylvester.precision. - * - * @param {number} x The value. - */ - snapTo(x: number): Matrix; - - /** - * Returns a string representation of the matrix. - */ - inspect(): string; - - /** - * Set the matrix's elements from an array. If the argument passed is a vector, the resulting matrix - * will be a single column. - * - * @param {Array|Array>|Vector|Matrix} matrix The elements. - */ - setElements(matrix: Array|Array>|Vector|Matrix): Matrix; -} - -interface Line { - /** - * Gets the 3D vector corresponding to a point on the line. - */ - anchor: Vector; - - /** - * Gets a normalized 3D vector representing the line's direction. - */ - direction: Vector; - - /** - * Returns true if the argument occupies the same space as the line. - * - * @param {Line} line The other line. - */ - eql(line: Line): boolean; - - /** - * Returns a copy of the line. - */ - dup(): Line; - - /** - * Returns the result of translating the line by the given vector/array. - * - * @param {Vector|Array} vector The translation vector. - */ - translate(vector: Vector|Array): Line; - - /** - * Returns true if the line is parallel to the argument. Here, 'parallel to' means that the argument's - * direction is either parallel or antiparallel to the line's own direction. A line is parallel to a - * plane if the two do not have a unique intersection. - * - * @param {Line|Plane} obj The object. - */ - isParallelTo(obj: Line|Plane): boolean; - - /** - * Returns the line's perpendicular distance from the argument, which can be a point, a line or a plane. - * - * @param {Vector|Line|Plane} obj The object. - */ - distanceFrom(obj: Vector|Line|Plane): number; - - /** - * Returns true if the argument is a point on the line. - * - * @param {Vector} point The point. - */ - contains(point: Vector): boolean; - - /** - * Returns true if the line lies in the given plane. - * - * @param {Plane} plane The plane. - */ - liesIn(plane: Plane): boolean; - - /** - * Returns true if the line has a unique point of intersection with the argument. - * - * @param {Line|Plane} obj The object. - */ - intersects(obj: Line|Plane): boolean; - - /** - * Returns the unique intersection point with the argument, if one exists. - * - * @param {Line|Plane} obj The object. - */ - intersectionWith(obj: Line|Plane): Vector; - - /** - * Returns the point on the line that is closest to the given point or line. - * - * @param {Vector|Line} obj The object. - */ - pointClosestTo(obj: Vector|Line): Vector; - - /** - * Returns a copy of the line rotated by t radians about the given line. Works by finding the argument's - * closest point to this line's anchor point (call this C) and rotating the anchor about C. Also rotates - * the line's direction about the argument's. Be careful with this - the rotation axis' direction - * affects the outcome! - * - * @param {number} t The angle in radians. - * @param {Vector|Line} axis The axis. - */ - rotate(t: number, axis: Vector|Line): Line; - - /** - * Returns the line's reflection in the given point or line. - * - * @param {Vector|Line|Plane} obj The object. - */ - reflectionIn(obj: Vector|Line|Plane): Line; - - /** - * Set the line's anchor point and direction. - * - * @param {Array|Vector} anchor The anchor vector. - * @param {Array|Vector} direction The direction vector. - */ - setVectors(anchor: Array|Vector, direction: Array|Vector): Line; -} - -interface Plane { - /** - * Gets the 3D vector corresponding to a point in the plane. - */ - anchor: Vector; - - /** - * Gets a normalized 3D vector perpendicular to the plane. - */ - normal: Vector; - - /** - * Returns true if the plane occupies the same space as the argument. - * - * @param {Plane} plane The other plane. - */ - eql(plane: Plane): boolean; - - /** - * Returns a copy of the plane. - */ - dup(): Plane; - - /** - * Returns the result of translating the plane by the given vector. - * - * @param {Array|Vector} vector The translation vector. - */ - translate(vector: Array|Vector): Plane; - - /** - * Returns true if the plane is parallel to the argument. Will return true if the planes are equal, - * or if you give a line and it lies in the plane. - * - * @param {Line|Plane} obj The object. - */ - isParallelTo(obj: Line|Plane): boolean; - - /** - * Returns true if the receiver is perpendicular to the argument. - * - * @param {Plane} plane The other plane. - */ - isPerpendicularTo(plane: Plane): boolean; - - /** - * Returns the plane's distance from the given object (point, line or plane). - * - * @parm {Vector|Line|Plane} obj The object. - */ - distanceFrom(obj: Vector|Line|Plane): number; - - /** - * Returns true if the plane contains the given point or line. - * - * @param {Vector|Line} obj The object. - */ - contains(obj: Vector|Line): boolean; - - /** - * Returns true if the plane has a unique point/line of intersection with the argument. - * - * @param {Line|Plane} obj The object. - */ - intersects(obj: Line|Plane): boolean; - - /** - * Returns the unique intersection with the argument, if one exists. - * - * @param {Line} line The line. - */ - intersectionWith(line: Line): Vector; - - /** - * Returns the unique intersection with the argument, if one exists. - * - * @param {Plane} plane The plane. - */ - intersectionWith(plane: Plane): Line; - - /** - * Returns the point in the plane closest to the given point. - * - * @param {Vector} point The point. - */ - pointClosestTo(point: Vector): Vector; - - /** - * Returns a copy of the plane, rotated by t radians about the given line. See notes on Line#rotate. - * - * @param {number} t The angle in radians. - * @param {Line} axis The line axis. - */ - rotate(t: number, axis: Line): Plane; - - /** - * Returns the reflection of the plane in the given point, line or plane. - * - * @param {Vector|Line|Plane} obj The object. - */ - reflectionIn(obj: Vector|Line|Plane): Plane; - - /** - * Sets the anchor point and normal to the plane. Normal vector is normalised before storage. - * - * @param {Array|Vector} anchor The anchor vector. - * @param {Array|Vector} normal The normal vector. - */ - setVectors(anchor: Array|Vector, normal: Array|Vector): Plane; - - /** - * Sets the anchor point and normal to the plane. The normal is calculated by assuming the three points - * should lie in the same plane. Normal vector is normalised before storage. - * - * @param {Array|Vector} anchor The anchor vector. - * @param {Array|Vector} v1 The first direction vector. - * @param {Array|Vector} v2 The second direction vector. - */ - setVectors(anchor: Array|Vector, v1: Array|Vector, v2: Array|Vector): Plane; -} - -declare module Sylvester { - export var version: string; - export var precision: number; -} - -declare var Vector: Sylvester.VectorStatic; -declare var Matrix: Sylvester.MatrixStatic; -declare var Line: Sylvester.LineStatic; -declare var Plane: Sylvester.PlaneStatic; - -/** -* Constructor function. -* -* @param {Vector|Array): Vector; - -/** -* Constructor function. -* -* @param {Array|Array>|Vector|Matrix} elements The elements. -*/ -declare function $M(elements: Array|Array>|Vector | Matrix): Matrix; - -/** -* Constructor function. -* -* @param Array|Vector anchor The anchor vector. -* @param Array|Vector direction The direction vector. -*/ -declare function $L(anchor: Array|Vector, direction: Array|Vector): Line; - -/** -* Constructor function. -* -* @param {Array|Vector} anchor The anchor vector. -* @param {Array|Vector} normal The normal vector. -*/ -declare function $P(anchor: Array|Vector, normal: Array|Vector): Plane; - -/** - * Constructor function. - * - * @param {Array|Vector} anchor The anchor vector. - * @param {Array|Vector} v1 The first direction vector. - * @param {Array|Vecotr} v2 The second direction vector. - */ -declare function $P(anchor: Array|Vector, v1: Array|Vector, v2: Array|Vector): Plane; diff -r 623836db99af -r 4556eebe5d73 light9/web/lib/tapmodo-Jcrop-1902fbc/MIT-LICENSE.txt --- a/light9/web/lib/tapmodo-Jcrop-1902fbc/MIT-LICENSE.txt Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,22 +0,0 @@ -Copyright (c) 2011 Tapmodo Interactive LLC, - http://github.com/tapmodo/Jcrop - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff -r 623836db99af -r 4556eebe5d73 light9/web/lib/tapmodo-Jcrop-1902fbc/README.md --- a/light9/web/lib/tapmodo-Jcrop-1902fbc/README.md Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,66 +0,0 @@ -Jcrop Image Cropping Plugin -=========================== - -Jcrop is the quick and easy way to add image cropping functionality to -your web application. It combines the ease-of-use of a typical jQuery -plugin with a powerful cross-platform DHTML cropping engine that is -faithful to familiar desktop graphics applications. - -Cross-platform Compatibility ----------------------------- - -* Firefox 2+ -* Safari 3+ -* Opera 9.5+ -* Google Chrome 0.2+ -* Internet Explorer 6+ - -Feature Overview ----------------- - -* Attaches unobtrusively to any image -* Supports aspect ratio locking -* Supports minSize/maxSize setting -* Callbacks for selection done, or while moving -* Keyboard support for nudging selection -* API features to create interactivity, including animation -* Support for CSS styling -* Experimental touch-screen support (iOS, Android, etc) - -Contributors -============ - -**Special thanks to the following contributors:** - -* [Bruno Agutoli](mailto:brunotla1@gmail.com) -* dhorrigan -* Phil-B -* jaymecd -* all others who have committed their time and effort to help improve Jcrop - -MIT License -=========== - -**Jcrop is free software under MIT License.** - -#### Copyright (c) 2008-2012 Tapmodo Interactive LLC,
http://github.com/tapmodo/Jcrop - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff -r 623836db99af -r 4556eebe5d73 light9/web/lib/tapmodo-Jcrop-1902fbc/css/Jcrop.gif Binary file light9/web/lib/tapmodo-Jcrop-1902fbc/css/Jcrop.gif has changed diff -r 623836db99af -r 4556eebe5d73 light9/web/lib/tapmodo-Jcrop-1902fbc/css/jquery.Jcrop.css --- a/light9/web/lib/tapmodo-Jcrop-1902fbc/css/jquery.Jcrop.css Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,165 +0,0 @@ -/* jquery.Jcrop.css v0.9.12 - MIT License */ -/* - The outer-most container in a typical Jcrop instance - If you are having difficulty with formatting related to styles - on a parent element, place any fixes here or in a like selector - - You can also style this element if you want to add a border, etc - A better method for styling can be seen below with .jcrop-light - (Add a class to the holder and style elements for that extended class) -*/ -.jcrop-holder { - direction: ltr; - text-align: left; -} -/* Selection Border */ -.jcrop-vline, -.jcrop-hline { - background: #ffffff url("Jcrop.gif"); - font-size: 0; - position: absolute; -} -.jcrop-vline { - height: 100%; - width: 1px !important; -} -.jcrop-vline.right { - right: 0; -} -.jcrop-hline { - height: 1px !important; - width: 100%; -} -.jcrop-hline.bottom { - bottom: 0; -} -/* Invisible click targets */ -.jcrop-tracker { - height: 100%; - width: 100%; - /* "turn off" link highlight */ - -webkit-tap-highlight-color: transparent; - /* disable callout, image save panel */ - -webkit-touch-callout: none; - /* disable cut copy paste */ - -webkit-user-select: none; -} -/* Selection Handles */ -.jcrop-handle { - background-color: #333333; - border: 1px #eeeeee solid; - width: 7px; - height: 7px; - font-size: 1px; -} -.jcrop-handle.ord-n { - left: 50%; - margin-left: -4px; - margin-top: -4px; - top: 0; -} -.jcrop-handle.ord-s { - bottom: 0; - left: 50%; - margin-bottom: -4px; - margin-left: -4px; -} -.jcrop-handle.ord-e { - margin-right: -4px; - margin-top: -4px; - right: 0; - top: 50%; -} -.jcrop-handle.ord-w { - left: 0; - margin-left: -4px; - margin-top: -4px; - top: 50%; -} -.jcrop-handle.ord-nw { - left: 0; - margin-left: -4px; - margin-top: -4px; - top: 0; -} -.jcrop-handle.ord-ne { - margin-right: -4px; - margin-top: -4px; - right: 0; - top: 0; -} -.jcrop-handle.ord-se { - bottom: 0; - margin-bottom: -4px; - margin-right: -4px; - right: 0; -} -.jcrop-handle.ord-sw { - bottom: 0; - left: 0; - margin-bottom: -4px; - margin-left: -4px; -} -/* Dragbars */ -.jcrop-dragbar.ord-n, -.jcrop-dragbar.ord-s { - height: 7px; - width: 100%; -} -.jcrop-dragbar.ord-e, -.jcrop-dragbar.ord-w { - height: 100%; - width: 7px; -} -.jcrop-dragbar.ord-n { - margin-top: -4px; -} -.jcrop-dragbar.ord-s { - bottom: 0; - margin-bottom: -4px; -} -.jcrop-dragbar.ord-e { - margin-right: -4px; - right: 0; -} -.jcrop-dragbar.ord-w { - margin-left: -4px; -} -/* The "jcrop-light" class/extension */ -.jcrop-light .jcrop-vline, -.jcrop-light .jcrop-hline { - background: #ffffff; - filter: alpha(opacity=70) !important; - opacity: .70!important; -} -.jcrop-light .jcrop-handle { - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - background-color: #000000; - border-color: #ffffff; - border-radius: 3px; -} -/* The "jcrop-dark" class/extension */ -.jcrop-dark .jcrop-vline, -.jcrop-dark .jcrop-hline { - background: #000000; - filter: alpha(opacity=70) !important; - opacity: 0.7 !important; -} -.jcrop-dark .jcrop-handle { - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - background-color: #ffffff; - border-color: #000000; - border-radius: 3px; -} -/* Simple macro to turn off the antlines */ -.solid-line .jcrop-vline, -.solid-line .jcrop-hline { - background: #ffffff; -} -/* Fix for twitter bootstrap et al. */ -.jcrop-holder img, -img.jcrop-preview { - max-width: none; -} diff -r 623836db99af -r 4556eebe5d73 light9/web/lib/tapmodo-Jcrop-1902fbc/css/jquery.Jcrop.min.css --- a/light9/web/lib/tapmodo-Jcrop-1902fbc/css/jquery.Jcrop.min.css Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,29 +0,0 @@ -/* jquery.Jcrop.min.css v0.9.12 (build:20130126) */ -.jcrop-holder{direction:ltr;text-align:left;} -.jcrop-vline,.jcrop-hline{background:#FFF url(Jcrop.gif);font-size:0;position:absolute;} -.jcrop-vline{height:100%;width:1px!important;} -.jcrop-vline.right{right:0;} -.jcrop-hline{height:1px!important;width:100%;} -.jcrop-hline.bottom{bottom:0;} -.jcrop-tracker{-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;-webkit-user-select:none;height:100%;width:100%;} -.jcrop-handle{background-color:#333;border:1px #EEE solid;font-size:1px;height:7px;width:7px;} -.jcrop-handle.ord-n{left:50%;margin-left:-4px;margin-top:-4px;top:0;} -.jcrop-handle.ord-s{bottom:0;left:50%;margin-bottom:-4px;margin-left:-4px;} -.jcrop-handle.ord-e{margin-right:-4px;margin-top:-4px;right:0;top:50%;} -.jcrop-handle.ord-w{left:0;margin-left:-4px;margin-top:-4px;top:50%;} -.jcrop-handle.ord-nw{left:0;margin-left:-4px;margin-top:-4px;top:0;} -.jcrop-handle.ord-ne{margin-right:-4px;margin-top:-4px;right:0;top:0;} -.jcrop-handle.ord-se{bottom:0;margin-bottom:-4px;margin-right:-4px;right:0;} -.jcrop-handle.ord-sw{bottom:0;left:0;margin-bottom:-4px;margin-left:-4px;} -.jcrop-dragbar.ord-n,.jcrop-dragbar.ord-s{height:7px;width:100%;} -.jcrop-dragbar.ord-e,.jcrop-dragbar.ord-w{height:100%;width:7px;} -.jcrop-dragbar.ord-n{margin-top:-4px;} -.jcrop-dragbar.ord-s{bottom:0;margin-bottom:-4px;} -.jcrop-dragbar.ord-e{margin-right:-4px;right:0;} -.jcrop-dragbar.ord-w{margin-left:-4px;} -.jcrop-light .jcrop-vline,.jcrop-light .jcrop-hline{background:#FFF;filter:alpha(opacity=70)!important;opacity:.70!important;} -.jcrop-light .jcrop-handle{-moz-border-radius:3px;-webkit-border-radius:3px;background-color:#000;border-color:#FFF;border-radius:3px;} -.jcrop-dark .jcrop-vline,.jcrop-dark .jcrop-hline{background:#000;filter:alpha(opacity=70)!important;opacity:.7!important;} -.jcrop-dark .jcrop-handle{-moz-border-radius:3px;-webkit-border-radius:3px;background-color:#FFF;border-color:#000;border-radius:3px;} -.solid-line .jcrop-vline,.solid-line .jcrop-hline{background:#FFF;} -.jcrop-holder img,img.jcrop-preview{max-width:none;} diff -r 623836db99af -r 4556eebe5d73 light9/web/lib/tapmodo-Jcrop-1902fbc/index.html --- a/light9/web/lib/tapmodo-Jcrop-1902fbc/index.html Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,84 +0,0 @@ - - - - Jcrop: the jQuery Image Cropping Plugin - - - - - - - -
-
-
-
- - - - - - Jcrop - is the image cropping plugin for - jQuery.
- You've successfully unpacked Jcrop. -
- -

Static Demos

- - - -

Live Demo

- - - -

Jcrop Links

- - - - -
- -
-
-
-
- - - diff -r 623836db99af -r 4556eebe5d73 light9/web/lib/tapmodo-Jcrop-1902fbc/js/jquery.Jcrop.js --- a/light9/web/lib/tapmodo-Jcrop-1902fbc/js/jquery.Jcrop.js Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1694 +0,0 @@ -/** - * jquery.Jcrop.js v0.9.12 - * jQuery Image Cropping Plugin - released under MIT License - * Author: Kelly Hallman - * http://github.com/tapmodo/Jcrop - * Copyright (c) 2008-2013 Tapmodo Interactive LLC {{{ - * - * Permission is hereby granted, free of charge, to any person - * obtaining a copy of this software and associated documentation - * files (the "Software"), to deal in the Software without - * restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following - * conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - * - * }}} - */ - -(function ($) { - - $.Jcrop = function (obj, opt) { - var options = $.extend({}, $.Jcrop.defaults), - docOffset, - _ua = navigator.userAgent.toLowerCase(), - is_msie = /msie/.test(_ua), - ie6mode = /msie [1-6]\./.test(_ua); - - // Internal Methods {{{ - function px(n) { - return Math.round(n) + 'px'; - } - function cssClass(cl) { - return options.baseClass + '-' + cl; - } - function supportsColorFade() { - return $.fx.step.hasOwnProperty('backgroundColor'); - } - function getPos(obj) //{{{ - { - var pos = $(obj).offset(); - return [pos.left, pos.top]; - } - //}}} - function mouseAbs(e) //{{{ - { - return [(e.pageX - docOffset[0]), (e.pageY - docOffset[1])]; - } - //}}} - function setOptions(opt) //{{{ - { - if (typeof(opt) !== 'object') opt = {}; - options = $.extend(options, opt); - - $.each(['onChange','onSelect','onRelease','onDblClick'],function(i,e) { - if (typeof(options[e]) !== 'function') options[e] = function () {}; - }); - } - //}}} - function startDragMode(mode, pos, touch) //{{{ - { - docOffset = getPos($img); - Tracker.setCursor(mode === 'move' ? mode : mode + '-resize'); - - if (mode === 'move') { - return Tracker.activateHandlers(createMover(pos), doneSelect, touch); - } - - var fc = Coords.getFixed(); - var opp = oppLockCorner(mode); - var opc = Coords.getCorner(oppLockCorner(opp)); - - Coords.setPressed(Coords.getCorner(opp)); - Coords.setCurrent(opc); - - Tracker.activateHandlers(dragmodeHandler(mode, fc), doneSelect, touch); - } - //}}} - function dragmodeHandler(mode, f) //{{{ - { - return function (pos) { - if (!options.aspectRatio) { - switch (mode) { - case 'e': - pos[1] = f.y2; - break; - case 'w': - pos[1] = f.y2; - break; - case 'n': - pos[0] = f.x2; - break; - case 's': - pos[0] = f.x2; - break; - } - } else { - switch (mode) { - case 'e': - pos[1] = f.y + 1; - break; - case 'w': - pos[1] = f.y + 1; - break; - case 'n': - pos[0] = f.x + 1; - break; - case 's': - pos[0] = f.x + 1; - break; - } - } - Coords.setCurrent(pos); - Selection.update(); - }; - } - //}}} - function createMover(pos) //{{{ - { - var lloc = pos; - KeyManager.watchKeys(); - - return function (pos) { - Coords.moveOffset([pos[0] - lloc[0], pos[1] - lloc[1]]); - lloc = pos; - - Selection.update(); - }; - } - //}}} - function oppLockCorner(ord) //{{{ - { - switch (ord) { - case 'n': - return 'sw'; - case 's': - return 'nw'; - case 'e': - return 'nw'; - case 'w': - return 'ne'; - case 'ne': - return 'sw'; - case 'nw': - return 'se'; - case 'se': - return 'nw'; - case 'sw': - return 'ne'; - } - } - //}}} - function createDragger(ord) //{{{ - { - return function (e) { - if (options.disabled) { - return false; - } - if ((ord === 'move') && !options.allowMove) { - return false; - } - - // Fix position of crop area when dragged the very first time. - // Necessary when crop image is in a hidden element when page is loaded. - docOffset = getPos($img); - - btndown = true; - startDragMode(ord, mouseAbs(e)); - e.stopPropagation(); - e.preventDefault(); - return false; - }; - } - //}}} - function presize($obj, w, h) //{{{ - { - var nw = $obj.width(), - nh = $obj.height(); - if ((nw > w) && w > 0) { - nw = w; - nh = (w / $obj.width()) * $obj.height(); - } - if ((nh > h) && h > 0) { - nh = h; - nw = (h / $obj.height()) * $obj.width(); - } - xscale = $obj.width() / nw; - yscale = $obj.height() / nh; - $obj.width(nw).height(nh); - } - //}}} - function unscale(c) //{{{ - { - return { - x: c.x * xscale, - y: c.y * yscale, - x2: c.x2 * xscale, - y2: c.y2 * yscale, - w: c.w * xscale, - h: c.h * yscale - }; - } - //}}} - function doneSelect(pos) //{{{ - { - var c = Coords.getFixed(); - if ((c.w > options.minSelect[0]) && (c.h > options.minSelect[1])) { - Selection.enableHandles(); - Selection.done(); - } else { - Selection.release(); - } - Tracker.setCursor(options.allowSelect ? 'crosshair' : 'default'); - } - //}}} - function newSelection(e) //{{{ - { - if (options.disabled) { - return false; - } - if (!options.allowSelect) { - return false; - } - btndown = true; - docOffset = getPos($img); - Selection.disableHandles(); - Tracker.setCursor('crosshair'); - var pos = mouseAbs(e); - Coords.setPressed(pos); - Selection.update(); - Tracker.activateHandlers(selectDrag, doneSelect, e.type.substring(0,5)==='touch'); - KeyManager.watchKeys(); - - e.stopPropagation(); - e.preventDefault(); - return false; - } - //}}} - function selectDrag(pos) //{{{ - { - Coords.setCurrent(pos); - Selection.update(); - } - //}}} - function newTracker() //{{{ - { - var trk = $('
').addClass(cssClass('tracker')); - if (is_msie) { - trk.css({ - opacity: 0, - backgroundColor: 'white' - }); - } - return trk; - } - //}}} - - // }}} - // Initialization {{{ - // Sanitize some options {{{ - if (typeof(obj) !== 'object') { - obj = $(obj)[0]; - } - if (typeof(opt) !== 'object') { - opt = {}; - } - // }}} - setOptions(opt); - // Initialize some jQuery objects {{{ - // The values are SET on the image(s) for the interface - // If the original image has any of these set, they will be reset - // However, if you destroy() the Jcrop instance the original image's - // character in the DOM will be as you left it. - var img_css = { - border: 'none', - visibility: 'visible', - margin: 0, - padding: 0, - position: 'absolute', - top: 0, - left: 0 - }; - - var $origimg = $(obj), - img_mode = true; - - if (obj.tagName == 'IMG') { - // Fix size of crop image. - // Necessary when crop image is within a hidden element when page is loaded. - if ($origimg[0].width != 0 && $origimg[0].height != 0) { - // Obtain dimensions from contained img element. - $origimg.width($origimg[0].width); - $origimg.height($origimg[0].height); - } else { - // Obtain dimensions from temporary image in case the original is not loaded yet (e.g. IE 7.0). - var tempImage = new Image(); - tempImage.src = $origimg[0].src; - $origimg.width(tempImage.width); - $origimg.height(tempImage.height); - } - - var $img = $origimg.clone().removeAttr('id').css(img_css).show(); - - $img.width($origimg.width()); - $img.height($origimg.height()); - $origimg.after($img).hide(); - - } else { - $img = $origimg.css(img_css).show(); - img_mode = false; - if (options.shade === null) { options.shade = true; } - } - - presize($img, options.boxWidth, options.boxHeight); - - var boundx = $img.width(), - boundy = $img.height(), - - - $div = $('
').width(boundx).height(boundy).addClass(cssClass('holder')).css({ - position: 'relative', - backgroundColor: options.bgColor - }).insertAfter($origimg).append($img); - - if (options.addClass) { - $div.addClass(options.addClass); - } - - var $img2 = $('
'), - - $img_holder = $('
') - .width('100%').height('100%').css({ - zIndex: 310, - position: 'absolute', - overflow: 'hidden' - }), - - $hdl_holder = $('
') - .width('100%').height('100%').css('zIndex', 320), - - $sel = $('
') - .css({ - position: 'absolute', - zIndex: 600 - }).dblclick(function(){ - var c = Coords.getFixed(); - options.onDblClick.call(api,c); - }).insertBefore($img).append($img_holder, $hdl_holder); - - if (img_mode) { - - $img2 = $('') - .attr('src', $img.attr('src')).css(img_css).width(boundx).height(boundy), - - $img_holder.append($img2); - - } - - if (ie6mode) { - $sel.css({ - overflowY: 'hidden' - }); - } - - var bound = options.boundary; - var $trk = newTracker().width(boundx + (bound * 2)).height(boundy + (bound * 2)).css({ - position: 'absolute', - top: px(-bound), - left: px(-bound), - zIndex: 290 - }).mousedown(newSelection); - - /* }}} */ - // Set more variables {{{ - var bgcolor = options.bgColor, - bgopacity = options.bgOpacity, - xlimit, ylimit, xmin, ymin, xscale, yscale, enabled = true, - btndown, animating, shift_down; - - docOffset = getPos($img); - // }}} - // }}} - // Internal Modules {{{ - // Touch Module {{{ - var Touch = (function () { - // Touch support detection function adapted (under MIT License) - // from code by Jeffrey Sambells - http://github.com/iamamused/ - function hasTouchSupport() { - var support = {}, events = ['touchstart', 'touchmove', 'touchend'], - el = document.createElement('div'), i; - - try { - for(i=0; i x1 + ox) { - ox -= ox + x1; - } - if (0 > y1 + oy) { - oy -= oy + y1; - } - - if (boundy < y2 + oy) { - oy += boundy - (y2 + oy); - } - if (boundx < x2 + ox) { - ox += boundx - (x2 + ox); - } - - x1 += ox; - x2 += ox; - y1 += oy; - y2 += oy; - } - //}}} - function getCorner(ord) //{{{ - { - var c = getFixed(); - switch (ord) { - case 'ne': - return [c.x2, c.y]; - case 'nw': - return [c.x, c.y]; - case 'se': - return [c.x2, c.y2]; - case 'sw': - return [c.x, c.y2]; - } - } - //}}} - function getFixed() //{{{ - { - if (!options.aspectRatio) { - return getRect(); - } - // This function could use some optimization I think... - var aspect = options.aspectRatio, - min_x = options.minSize[0] / xscale, - - - //min_y = options.minSize[1]/yscale, - max_x = options.maxSize[0] / xscale, - max_y = options.maxSize[1] / yscale, - rw = x2 - x1, - rh = y2 - y1, - rwa = Math.abs(rw), - rha = Math.abs(rh), - real_ratio = rwa / rha, - xx, yy, w, h; - - if (max_x === 0) { - max_x = boundx * 10; - } - if (max_y === 0) { - max_y = boundy * 10; - } - if (real_ratio < aspect) { - yy = y2; - w = rha * aspect; - xx = rw < 0 ? x1 - w : w + x1; - - if (xx < 0) { - xx = 0; - h = Math.abs((xx - x1) / aspect); - yy = rh < 0 ? y1 - h : h + y1; - } else if (xx > boundx) { - xx = boundx; - h = Math.abs((xx - x1) / aspect); - yy = rh < 0 ? y1 - h : h + y1; - } - } else { - xx = x2; - h = rwa / aspect; - yy = rh < 0 ? y1 - h : y1 + h; - if (yy < 0) { - yy = 0; - w = Math.abs((yy - y1) * aspect); - xx = rw < 0 ? x1 - w : w + x1; - } else if (yy > boundy) { - yy = boundy; - w = Math.abs(yy - y1) * aspect; - xx = rw < 0 ? x1 - w : w + x1; - } - } - - // Magic %-) - if (xx > x1) { // right side - if (xx - x1 < min_x) { - xx = x1 + min_x; - } else if (xx - x1 > max_x) { - xx = x1 + max_x; - } - if (yy > y1) { - yy = y1 + (xx - x1) / aspect; - } else { - yy = y1 - (xx - x1) / aspect; - } - } else if (xx < x1) { // left side - if (x1 - xx < min_x) { - xx = x1 - min_x; - } else if (x1 - xx > max_x) { - xx = x1 - max_x; - } - if (yy > y1) { - yy = y1 + (x1 - xx) / aspect; - } else { - yy = y1 - (x1 - xx) / aspect; - } - } - - if (xx < 0) { - x1 -= xx; - xx = 0; - } else if (xx > boundx) { - x1 -= xx - boundx; - xx = boundx; - } - - if (yy < 0) { - y1 -= yy; - yy = 0; - } else if (yy > boundy) { - y1 -= yy - boundy; - yy = boundy; - } - - return makeObj(flipCoords(x1, y1, xx, yy)); - } - //}}} - function rebound(p) //{{{ - { - if (p[0] < 0) p[0] = 0; - if (p[1] < 0) p[1] = 0; - - if (p[0] > boundx) p[0] = boundx; - if (p[1] > boundy) p[1] = boundy; - - return [Math.round(p[0]), Math.round(p[1])]; - } - //}}} - function flipCoords(x1, y1, x2, y2) //{{{ - { - var xa = x1, - xb = x2, - ya = y1, - yb = y2; - if (x2 < x1) { - xa = x2; - xb = x1; - } - if (y2 < y1) { - ya = y2; - yb = y1; - } - return [xa, ya, xb, yb]; - } - //}}} - function getRect() //{{{ - { - var xsize = x2 - x1, - ysize = y2 - y1, - delta; - - if (xlimit && (Math.abs(xsize) > xlimit)) { - x2 = (xsize > 0) ? (x1 + xlimit) : (x1 - xlimit); - } - if (ylimit && (Math.abs(ysize) > ylimit)) { - y2 = (ysize > 0) ? (y1 + ylimit) : (y1 - ylimit); - } - - if (ymin / yscale && (Math.abs(ysize) < ymin / yscale)) { - y2 = (ysize > 0) ? (y1 + ymin / yscale) : (y1 - ymin / yscale); - } - if (xmin / xscale && (Math.abs(xsize) < xmin / xscale)) { - x2 = (xsize > 0) ? (x1 + xmin / xscale) : (x1 - xmin / xscale); - } - - if (x1 < 0) { - x2 -= x1; - x1 -= x1; - } - if (y1 < 0) { - y2 -= y1; - y1 -= y1; - } - if (x2 < 0) { - x1 -= x2; - x2 -= x2; - } - if (y2 < 0) { - y1 -= y2; - y2 -= y2; - } - if (x2 > boundx) { - delta = x2 - boundx; - x1 -= delta; - x2 -= delta; - } - if (y2 > boundy) { - delta = y2 - boundy; - y1 -= delta; - y2 -= delta; - } - if (x1 > boundx) { - delta = x1 - boundy; - y2 -= delta; - y1 -= delta; - } - if (y1 > boundy) { - delta = y1 - boundy; - y2 -= delta; - y1 -= delta; - } - - return makeObj(flipCoords(x1, y1, x2, y2)); - } - //}}} - function makeObj(a) //{{{ - { - return { - x: a[0], - y: a[1], - x2: a[2], - y2: a[3], - w: a[2] - a[0], - h: a[3] - a[1] - }; - } - //}}} - - return { - flipCoords: flipCoords, - setPressed: setPressed, - setCurrent: setCurrent, - getOffset: getOffset, - moveOffset: moveOffset, - getCorner: getCorner, - getFixed: getFixed - }; - }()); - - //}}} - // Shade Module {{{ - var Shade = (function() { - var enabled = false, - holder = $('
').css({ - position: 'absolute', - zIndex: 240, - opacity: 0 - }), - shades = { - top: createShade(), - left: createShade().height(boundy), - right: createShade().height(boundy), - bottom: createShade() - }; - - function resizeShades(w,h) { - shades.left.css({ height: px(h) }); - shades.right.css({ height: px(h) }); - } - function updateAuto() - { - return updateShade(Coords.getFixed()); - } - function updateShade(c) - { - shades.top.css({ - left: px(c.x), - width: px(c.w), - height: px(c.y) - }); - shades.bottom.css({ - top: px(c.y2), - left: px(c.x), - width: px(c.w), - height: px(boundy-c.y2) - }); - shades.right.css({ - left: px(c.x2), - width: px(boundx-c.x2) - }); - shades.left.css({ - width: px(c.x) - }); - } - function createShade() { - return $('
').css({ - position: 'absolute', - backgroundColor: options.shadeColor||options.bgColor - }).appendTo(holder); - } - function enableShade() { - if (!enabled) { - enabled = true; - holder.insertBefore($img); - updateAuto(); - Selection.setBgOpacity(1,0,1); - $img2.hide(); - - setBgColor(options.shadeColor||options.bgColor,1); - if (Selection.isAwake()) - { - setOpacity(options.bgOpacity,1); - } - else setOpacity(1,1); - } - } - function setBgColor(color,now) { - colorChangeMacro(getShades(),color,now); - } - function disableShade() { - if (enabled) { - holder.remove(); - $img2.show(); - enabled = false; - if (Selection.isAwake()) { - Selection.setBgOpacity(options.bgOpacity,1,1); - } else { - Selection.setBgOpacity(1,1,1); - Selection.disableHandles(); - } - colorChangeMacro($div,0,1); - } - } - function setOpacity(opacity,now) { - if (enabled) { - if (options.bgFade && !now) { - holder.animate({ - opacity: 1-opacity - },{ - queue: false, - duration: options.fadeTime - }); - } - else holder.css({opacity:1-opacity}); - } - } - function refreshAll() { - options.shade ? enableShade() : disableShade(); - if (Selection.isAwake()) setOpacity(options.bgOpacity); - } - function getShades() { - return holder.children(); - } - - return { - update: updateAuto, - updateRaw: updateShade, - getShades: getShades, - setBgColor: setBgColor, - enable: enableShade, - disable: disableShade, - resize: resizeShades, - refresh: refreshAll, - opacity: setOpacity - }; - }()); - // }}} - // Selection Module {{{ - var Selection = (function () { - var awake, - hdep = 370, - borders = {}, - handle = {}, - dragbar = {}, - seehandles = false; - - // Private Methods - function insertBorder(type) //{{{ - { - var jq = $('
').css({ - position: 'absolute', - opacity: options.borderOpacity - }).addClass(cssClass(type)); - $img_holder.append(jq); - return jq; - } - //}}} - function dragDiv(ord, zi) //{{{ - { - var jq = $('
').mousedown(createDragger(ord)).css({ - cursor: ord + '-resize', - position: 'absolute', - zIndex: zi - }).addClass('ord-'+ord); - - if (Touch.support) { - jq.bind('touchstart.jcrop', Touch.createDragger(ord)); - } - - $hdl_holder.append(jq); - return jq; - } - //}}} - function insertHandle(ord) //{{{ - { - var hs = options.handleSize, - - div = dragDiv(ord, hdep++).css({ - opacity: options.handleOpacity - }).addClass(cssClass('handle')); - - if (hs) { div.width(hs).height(hs); } - - return div; - } - //}}} - function insertDragbar(ord) //{{{ - { - return dragDiv(ord, hdep++).addClass('jcrop-dragbar'); - } - //}}} - function createDragbars(li) //{{{ - { - var i; - for (i = 0; i < li.length; i++) { - dragbar[li[i]] = insertDragbar(li[i]); - } - } - //}}} - function createBorders(li) //{{{ - { - var cl,i; - for (i = 0; i < li.length; i++) { - switch(li[i]){ - case'n': cl='hline'; break; - case's': cl='hline bottom'; break; - case'e': cl='vline right'; break; - case'w': cl='vline'; break; - } - borders[li[i]] = insertBorder(cl); - } - } - //}}} - function createHandles(li) //{{{ - { - var i; - for (i = 0; i < li.length; i++) { - handle[li[i]] = insertHandle(li[i]); - } - } - //}}} - function moveto(x, y) //{{{ - { - if (!options.shade) { - $img2.css({ - top: px(-y), - left: px(-x) - }); - } - $sel.css({ - top: px(y), - left: px(x) - }); - } - //}}} - function resize(w, h) //{{{ - { - $sel.width(Math.round(w)).height(Math.round(h)); - } - //}}} - function refresh() //{{{ - { - var c = Coords.getFixed(); - - Coords.setPressed([c.x, c.y]); - Coords.setCurrent([c.x2, c.y2]); - - updateVisible(); - } - //}}} - - // Internal Methods - function updateVisible(select) //{{{ - { - if (awake) { - return update(select); - } - } - //}}} - function update(select) //{{{ - { - var c = Coords.getFixed(); - - resize(c.w, c.h); - moveto(c.x, c.y); - if (options.shade) Shade.updateRaw(c); - - awake || show(); - - if (select) { - options.onSelect.call(api, unscale(c)); - } else { - options.onChange.call(api, unscale(c)); - } - } - //}}} - function setBgOpacity(opacity,force,now) //{{{ - { - if (!awake && !force) return; - if (options.bgFade && !now) { - $img.animate({ - opacity: opacity - },{ - queue: false, - duration: options.fadeTime - }); - } else { - $img.css('opacity', opacity); - } - } - //}}} - function show() //{{{ - { - $sel.show(); - - if (options.shade) Shade.opacity(bgopacity); - else setBgOpacity(bgopacity,true); - - awake = true; - } - //}}} - function release() //{{{ - { - disableHandles(); - $sel.hide(); - - if (options.shade) Shade.opacity(1); - else setBgOpacity(1); - - awake = false; - options.onRelease.call(api); - } - //}}} - function showHandles() //{{{ - { - if (seehandles) { - $hdl_holder.show(); - } - } - //}}} - function enableHandles() //{{{ - { - seehandles = true; - if (options.allowResize) { - $hdl_holder.show(); - return true; - } - } - //}}} - function disableHandles() //{{{ - { - seehandles = false; - $hdl_holder.hide(); - } - //}}} - function animMode(v) //{{{ - { - if (v) { - animating = true; - disableHandles(); - } else { - animating = false; - enableHandles(); - } - } - //}}} - function done() //{{{ - { - animMode(false); - refresh(); - } - //}}} - // Insert draggable elements {{{ - // Insert border divs for outline - - if (options.dragEdges && $.isArray(options.createDragbars)) - createDragbars(options.createDragbars); - - if ($.isArray(options.createHandles)) - createHandles(options.createHandles); - - if (options.drawBorders && $.isArray(options.createBorders)) - createBorders(options.createBorders); - - //}}} - - // This is a hack for iOS5 to support drag/move touch functionality - $(document).bind('touchstart.jcrop-ios',function(e) { - if ($(e.currentTarget).hasClass('jcrop-tracker')) e.stopPropagation(); - }); - - var $track = newTracker().mousedown(createDragger('move')).css({ - cursor: 'move', - position: 'absolute', - zIndex: 360 - }); - - if (Touch.support) { - $track.bind('touchstart.jcrop', Touch.createDragger('move')); - } - - $img_holder.append($track); - disableHandles(); - - return { - updateVisible: updateVisible, - update: update, - release: release, - refresh: refresh, - isAwake: function () { - return awake; - }, - setCursor: function (cursor) { - $track.css('cursor', cursor); - }, - enableHandles: enableHandles, - enableOnly: function () { - seehandles = true; - }, - showHandles: showHandles, - disableHandles: disableHandles, - animMode: animMode, - setBgOpacity: setBgOpacity, - done: done - }; - }()); - - //}}} - // Tracker Module {{{ - var Tracker = (function () { - var onMove = function () {}, - onDone = function () {}, - trackDoc = options.trackDocument; - - function toFront(touch) //{{{ - { - $trk.css({ - zIndex: 450 - }); - - if (touch) - $(document) - .bind('touchmove.jcrop', trackTouchMove) - .bind('touchend.jcrop', trackTouchEnd); - - else if (trackDoc) - $(document) - .bind('mousemove.jcrop',trackMove) - .bind('mouseup.jcrop',trackUp); - } - //}}} - function toBack() //{{{ - { - $trk.css({ - zIndex: 290 - }); - $(document).unbind('.jcrop'); - } - //}}} - function trackMove(e) //{{{ - { - onMove(mouseAbs(e)); - return false; - } - //}}} - function trackUp(e) //{{{ - { - e.preventDefault(); - e.stopPropagation(); - - if (btndown) { - btndown = false; - - onDone(mouseAbs(e)); - - if (Selection.isAwake()) { - options.onSelect.call(api, unscale(Coords.getFixed())); - } - - toBack(); - onMove = function () {}; - onDone = function () {}; - } - - return false; - } - //}}} - function activateHandlers(move, done, touch) //{{{ - { - btndown = true; - onMove = move; - onDone = done; - toFront(touch); - return false; - } - //}}} - function trackTouchMove(e) //{{{ - { - onMove(mouseAbs(Touch.cfilter(e))); - return false; - } - //}}} - function trackTouchEnd(e) //{{{ - { - return trackUp(Touch.cfilter(e)); - } - //}}} - function setCursor(t) //{{{ - { - $trk.css('cursor', t); - } - //}}} - - if (!trackDoc) { - $trk.mousemove(trackMove).mouseup(trackUp).mouseout(trackUp); - } - - $img.before($trk); - return { - activateHandlers: activateHandlers, - setCursor: setCursor - }; - }()); - //}}} - // KeyManager Module {{{ - var KeyManager = (function () { - var $keymgr = $('').css({ - position: 'fixed', - left: '-120px', - width: '12px' - }).addClass('jcrop-keymgr'), - - $keywrap = $('
').css({ - position: 'absolute', - overflow: 'hidden' - }).append($keymgr); - - function watchKeys() //{{{ - { - if (options.keySupport) { - $keymgr.show(); - $keymgr.focus(); - } - } - //}}} - function onBlur(e) //{{{ - { - $keymgr.hide(); - } - //}}} - function doNudge(e, x, y) //{{{ - { - if (options.allowMove) { - Coords.moveOffset([x, y]); - Selection.updateVisible(true); - } - e.preventDefault(); - e.stopPropagation(); - } - //}}} - function parseKey(e) //{{{ - { - if (e.ctrlKey || e.metaKey) { - return true; - } - shift_down = e.shiftKey ? true : false; - var nudge = shift_down ? 10 : 1; - - switch (e.keyCode) { - case 37: - doNudge(e, -nudge, 0); - break; - case 39: - doNudge(e, nudge, 0); - break; - case 38: - doNudge(e, 0, -nudge); - break; - case 40: - doNudge(e, 0, nudge); - break; - case 27: - if (options.allowSelect) Selection.release(); - break; - case 9: - return true; - } - - return false; - } - //}}} - - if (options.keySupport) { - $keymgr.keydown(parseKey).blur(onBlur); - if (ie6mode || !options.fixedSupport) { - $keymgr.css({ - position: 'absolute', - left: '-20px' - }); - $keywrap.append($keymgr).insertBefore($img); - } else { - $keymgr.insertBefore($img); - } - } - - - return { - watchKeys: watchKeys - }; - }()); - //}}} - // }}} - // API methods {{{ - function setClass(cname) //{{{ - { - $div.removeClass().addClass(cssClass('holder')).addClass(cname); - } - //}}} - function animateTo(a, callback) //{{{ - { - var x1 = a[0] / xscale, - y1 = a[1] / yscale, - x2 = a[2] / xscale, - y2 = a[3] / yscale; - - if (animating) { - return; - } - - var animto = Coords.flipCoords(x1, y1, x2, y2), - c = Coords.getFixed(), - initcr = [c.x, c.y, c.x2, c.y2], - animat = initcr, - interv = options.animationDelay, - ix1 = animto[0] - initcr[0], - iy1 = animto[1] - initcr[1], - ix2 = animto[2] - initcr[2], - iy2 = animto[3] - initcr[3], - pcent = 0, - velocity = options.swingSpeed; - - x1 = animat[0]; - y1 = animat[1]; - x2 = animat[2]; - y2 = animat[3]; - - Selection.animMode(true); - var anim_timer; - - function queueAnimator() { - window.setTimeout(animator, interv); - } - var animator = (function () { - return function () { - pcent += (100 - pcent) / velocity; - - animat[0] = Math.round(x1 + ((pcent / 100) * ix1)); - animat[1] = Math.round(y1 + ((pcent / 100) * iy1)); - animat[2] = Math.round(x2 + ((pcent / 100) * ix2)); - animat[3] = Math.round(y2 + ((pcent / 100) * iy2)); - - if (pcent >= 99.8) { - pcent = 100; - } - if (pcent < 100) { - setSelectRaw(animat); - queueAnimator(); - } else { - Selection.done(); - Selection.animMode(false); - if (typeof(callback) === 'function') { - callback.call(api); - } - } - }; - }()); - queueAnimator(); - } - //}}} - function setSelect(rect) //{{{ - { - setSelectRaw([rect[0] / xscale, rect[1] / yscale, rect[2] / xscale, rect[3] / yscale]); - options.onSelect.call(api, unscale(Coords.getFixed())); - Selection.enableHandles(); - } - //}}} - function setSelectRaw(l) //{{{ - { - Coords.setPressed([l[0], l[1]]); - Coords.setCurrent([l[2], l[3]]); - Selection.update(); - } - //}}} - function tellSelect() //{{{ - { - return unscale(Coords.getFixed()); - } - //}}} - function tellScaled() //{{{ - { - return Coords.getFixed(); - } - //}}} - function setOptionsNew(opt) //{{{ - { - setOptions(opt); - interfaceUpdate(); - } - //}}} - function disableCrop() //{{{ - { - options.disabled = true; - Selection.disableHandles(); - Selection.setCursor('default'); - Tracker.setCursor('default'); - } - //}}} - function enableCrop() //{{{ - { - options.disabled = false; - interfaceUpdate(); - } - //}}} - function cancelCrop() //{{{ - { - Selection.done(); - Tracker.activateHandlers(null, null); - } - //}}} - function destroy() //{{{ - { - $div.remove(); - $origimg.show(); - $origimg.css('visibility','visible'); - $(obj).removeData('Jcrop'); - } - //}}} - function setImage(src, callback) //{{{ - { - Selection.release(); - disableCrop(); - var img = new Image(); - img.onload = function () { - var iw = img.width; - var ih = img.height; - var bw = options.boxWidth; - var bh = options.boxHeight; - $img.width(iw).height(ih); - $img.attr('src', src); - $img2.attr('src', src); - presize($img, bw, bh); - boundx = $img.width(); - boundy = $img.height(); - $img2.width(boundx).height(boundy); - $trk.width(boundx + (bound * 2)).height(boundy + (bound * 2)); - $div.width(boundx).height(boundy); - Shade.resize(boundx,boundy); - enableCrop(); - - if (typeof(callback) === 'function') { - callback.call(api); - } - }; - img.src = src; - } - //}}} - function colorChangeMacro($obj,color,now) { - var mycolor = color || options.bgColor; - if (options.bgFade && supportsColorFade() && options.fadeTime && !now) { - $obj.animate({ - backgroundColor: mycolor - }, { - queue: false, - duration: options.fadeTime - }); - } else { - $obj.css('backgroundColor', mycolor); - } - } - function interfaceUpdate(alt) //{{{ - // This method tweaks the interface based on options object. - // Called when options are changed and at end of initialization. - { - if (options.allowResize) { - if (alt) { - Selection.enableOnly(); - } else { - Selection.enableHandles(); - } - } else { - Selection.disableHandles(); - } - - Tracker.setCursor(options.allowSelect ? 'crosshair' : 'default'); - Selection.setCursor(options.allowMove ? 'move' : 'default'); - - if (options.hasOwnProperty('trueSize')) { - xscale = options.trueSize[0] / boundx; - yscale = options.trueSize[1] / boundy; - } - - if (options.hasOwnProperty('setSelect')) { - setSelect(options.setSelect); - Selection.done(); - delete(options.setSelect); - } - - Shade.refresh(); - - if (options.bgColor != bgcolor) { - colorChangeMacro( - options.shade? Shade.getShades(): $div, - options.shade? - (options.shadeColor || options.bgColor): - options.bgColor - ); - bgcolor = options.bgColor; - } - - if (bgopacity != options.bgOpacity) { - bgopacity = options.bgOpacity; - if (options.shade) Shade.refresh(); - else Selection.setBgOpacity(bgopacity); - } - - xlimit = options.maxSize[0] || 0; - ylimit = options.maxSize[1] || 0; - xmin = options.minSize[0] || 0; - ymin = options.minSize[1] || 0; - - if (options.hasOwnProperty('outerImage')) { - $img.attr('src', options.outerImage); - delete(options.outerImage); - } - - Selection.refresh(); - } - //}}} - //}}} - - if (Touch.support) $trk.bind('touchstart.jcrop', Touch.newSelection); - - $hdl_holder.hide(); - interfaceUpdate(true); - - var api = { - setImage: setImage, - animateTo: animateTo, - setSelect: setSelect, - setOptions: setOptionsNew, - tellSelect: tellSelect, - tellScaled: tellScaled, - setClass: setClass, - - disable: disableCrop, - enable: enableCrop, - cancel: cancelCrop, - release: Selection.release, - destroy: destroy, - - focus: KeyManager.watchKeys, - - getBounds: function () { - return [boundx * xscale, boundy * yscale]; - }, - getWidgetSize: function () { - return [boundx, boundy]; - }, - getScaleFactor: function () { - return [xscale, yscale]; - }, - getOptions: function() { - // careful: internal values are returned - return options; - }, - - ui: { - holder: $div, - selection: $sel - } - }; - - if (is_msie) $div.bind('selectstart', function () { return false; }); - - $origimg.data('Jcrop', api); - return api; - }; - $.fn.Jcrop = function (options, callback) //{{{ - { - var api; - // Iterate over each object, attach Jcrop - this.each(function () { - // If we've already attached to this object - if ($(this).data('Jcrop')) { - // The API can be requested this way (undocumented) - if (options === 'api') return $(this).data('Jcrop'); - // Otherwise, we just reset the options... - else $(this).data('Jcrop').setOptions(options); - } - // If we haven't been attached, preload and attach - else { - if (this.tagName == 'IMG') - $.Jcrop.Loader(this,function(){ - $(this).css({display:'block',visibility:'hidden'}); - api = $.Jcrop(this, options); - if ($.isFunction(callback)) callback.call(api); - }); - else { - $(this).css({display:'block',visibility:'hidden'}); - api = $.Jcrop(this, options); - if ($.isFunction(callback)) callback.call(api); - } - } - }); - - // Return "this" so the object is chainable (jQuery-style) - return this; - }; - //}}} - // $.Jcrop.Loader - basic image loader {{{ - - $.Jcrop.Loader = function(imgobj,success,error){ - var $img = $(imgobj), img = $img[0]; - - function completeCheck(){ - if (img.complete) { - $img.unbind('.jcloader'); - if ($.isFunction(success)) success.call(img); - } - else window.setTimeout(completeCheck,50); - } - - $img - .bind('load.jcloader',completeCheck) - .bind('error.jcloader',function(e){ - $img.unbind('.jcloader'); - if ($.isFunction(error)) error.call(img); - }); - - if (img.complete && $.isFunction(success)){ - $img.unbind('.jcloader'); - success.call(img); - } - }; - - //}}} - // Global Defaults {{{ - $.Jcrop.defaults = { - - // Basic Settings - allowSelect: true, - allowMove: true, - allowResize: true, - - trackDocument: true, - - // Styling Options - baseClass: 'jcrop', - addClass: null, - bgColor: 'black', - bgOpacity: 0.6, - bgFade: false, - borderOpacity: 0.4, - handleOpacity: 0.5, - handleSize: null, - - aspectRatio: 0, - keySupport: true, - createHandles: ['n','s','e','w','nw','ne','se','sw'], - createDragbars: ['n','s','e','w'], - createBorders: ['n','s','e','w'], - drawBorders: true, - dragEdges: true, - fixedSupport: true, - touchSupport: null, - - shade: null, - - boxWidth: 0, - boxHeight: 0, - boundary: 2, - fadeTime: 400, - animationDelay: 20, - swingSpeed: 3, - - minSelect: [0, 0], - maxSize: [0, 0], - minSize: [0, 0], - - // Callbacks / Event Handlers - onChange: function () {}, - onSelect: function () {}, - onDblClick: function () {}, - onRelease: function () {} - }; - - // }}} -}(jQuery)); diff -r 623836db99af -r 4556eebe5d73 light9/web/lib/tapmodo-Jcrop-1902fbc/js/jquery.Jcrop.min.js --- a/light9/web/lib/tapmodo-Jcrop-1902fbc/js/jquery.Jcrop.min.js Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,22 +0,0 @@ -/** - * jquery.Jcrop.min.js v0.9.12 (build:20130202) - * jQuery Image Cropping Plugin - released under MIT License - * Copyright (c) 2008-2013 Tapmodo Interactive LLC - * https://github.com/tapmodo/Jcrop - */ -(function(a){a.Jcrop=function(b,c){function i(a){return Math.round(a)+"px"}function j(a){return d.baseClass+"-"+a}function k(){return a.fx.step.hasOwnProperty("backgroundColor")}function l(b){var c=a(b).offset();return[c.left,c.top]}function m(a){return[a.pageX-e[0],a.pageY-e[1]]}function n(b){typeof b!="object"&&(b={}),d=a.extend(d,b),a.each(["onChange","onSelect","onRelease","onDblClick"],function(a,b){typeof d[b]!="function"&&(d[b]=function(){})})}function o(a,b,c){e=l(D),bc.setCursor(a==="move"?a:a+"-resize");if(a==="move")return bc.activateHandlers(q(b),v,c);var d=_.getFixed(),f=r(a),g=_.getCorner(r(f));_.setPressed(_.getCorner(f)),_.setCurrent(g),bc.activateHandlers(p(a,d),v,c)}function p(a,b){return function(c){if(!d.aspectRatio)switch(a){case"e":c[1]=b.y2;break;case"w":c[1]=b.y2;break;case"n":c[0]=b.x2;break;case"s":c[0]=b.x2}else switch(a){case"e":c[1]=b.y+1;break;case"w":c[1]=b.y+1;break;case"n":c[0]=b.x+1;break;case"s":c[0]=b.x+1}_.setCurrent(c),bb.update()}}function q(a){var b=a;return bd.watchKeys -(),function(a){_.moveOffset([a[0]-b[0],a[1]-b[1]]),b=a,bb.update()}}function r(a){switch(a){case"n":return"sw";case"s":return"nw";case"e":return"nw";case"w":return"ne";case"ne":return"sw";case"nw":return"se";case"se":return"nw";case"sw":return"ne"}}function s(a){return function(b){return d.disabled?!1:a==="move"&&!d.allowMove?!1:(e=l(D),W=!0,o(a,m(b)),b.stopPropagation(),b.preventDefault(),!1)}}function t(a,b,c){var d=a.width(),e=a.height();d>b&&b>0&&(d=b,e=b/a.width()*a.height()),e>c&&c>0&&(e=c,d=c/a.height()*a.width()),T=a.width()/d,U=a.height()/e,a.width(d).height(e)}function u(a){return{x:a.x*T,y:a.y*U,x2:a.x2*T,y2:a.y2*U,w:a.w*T,h:a.h*U}}function v(a){var b=_.getFixed();b.w>d.minSelect[0]&&b.h>d.minSelect[1]?(bb.enableHandles(),bb.done()):bb.release(),bc.setCursor(d.allowSelect?"crosshair":"default")}function w(a){if(d.disabled)return!1;if(!d.allowSelect)return!1;W=!0,e=l(D),bb.disableHandles(),bc.setCursor("crosshair");var b=m(a);return _.setPressed(b),bb.update(),bc.activateHandlers(x,v,a.type.substring -(0,5)==="touch"),bd.watchKeys(),a.stopPropagation(),a.preventDefault(),!1}function x(a){_.setCurrent(a),bb.update()}function y(){var b=a("
").addClass(j("tracker"));return g&&b.css({opacity:0,backgroundColor:"white"}),b}function be(a){G.removeClass().addClass(j("holder")).addClass(a)}function bf(a,b){function t(){window.setTimeout(u,l)}var c=a[0]/T,e=a[1]/U,f=a[2]/T,g=a[3]/U;if(X)return;var h=_.flipCoords(c,e,f,g),i=_.getFixed(),j=[i.x,i.y,i.x2,i.y2],k=j,l=d.animationDelay,m=h[0]-j[0],n=h[1]-j[1],o=h[2]-j[2],p=h[3]-j[3],q=0,r=d.swingSpeed;c=k[0],e=k[1],f=k[2],g=k[3],bb.animMode(!0);var s,u=function(){return function(){q+=(100-q)/r,k[0]=Math.round(c+q/100*m),k[1]=Math.round(e+q/100*n),k[2]=Math.round(f+q/100*o),k[3]=Math.round(g+q/100*p),q>=99.8&&(q=100),q<100?(bh(k),t()):(bb.done(),bb.animMode(!1),typeof b=="function"&&b.call(bs))}}();t()}function bg(a){bh([a[0]/T,a[1]/U,a[2]/T,a[3]/U]),d.onSelect.call(bs,u(_.getFixed())),bb.enableHandles()}function bh(a){_.setPressed([a[0],a[1]]),_.setCurrent([a[2], -a[3]]),bb.update()}function bi(){return u(_.getFixed())}function bj(){return _.getFixed()}function bk(a){n(a),br()}function bl(){d.disabled=!0,bb.disableHandles(),bb.setCursor("default"),bc.setCursor("default")}function bm(){d.disabled=!1,br()}function bn(){bb.done(),bc.activateHandlers(null,null)}function bo(){G.remove(),A.show(),A.css("visibility","visible"),a(b).removeData("Jcrop")}function bp(a,b){bb.release(),bl();var c=new Image;c.onload=function(){var e=c.width,f=c.height,g=d.boxWidth,h=d.boxHeight;D.width(e).height(f),D.attr("src",a),H.attr("src",a),t(D,g,h),E=D.width(),F=D.height(),H.width(E).height(F),M.width(E+L*2).height(F+L*2),G.width(E).height(F),ba.resize(E,F),bm(),typeof b=="function"&&b.call(bs)},c.src=a}function bq(a,b,c){var e=b||d.bgColor;d.bgFade&&k()&&d.fadeTime&&!c?a.animate({backgroundColor:e},{queue:!1,duration:d.fadeTime}):a.css("backgroundColor",e)}function br(a){d.allowResize?a?bb.enableOnly():bb.enableHandles():bb.disableHandles(),bc.setCursor(d.allowSelect?"crosshair":"default"),bb -.setCursor(d.allowMove?"move":"default"),d.hasOwnProperty("trueSize")&&(T=d.trueSize[0]/E,U=d.trueSize[1]/F),d.hasOwnProperty("setSelect")&&(bg(d.setSelect),bb.done(),delete d.setSelect),ba.refresh(),d.bgColor!=N&&(bq(d.shade?ba.getShades():G,d.shade?d.shadeColor||d.bgColor:d.bgColor),N=d.bgColor),O!=d.bgOpacity&&(O=d.bgOpacity,d.shade?ba.refresh():bb.setBgOpacity(O)),P=d.maxSize[0]||0,Q=d.maxSize[1]||0,R=d.minSize[0]||0,S=d.minSize[1]||0,d.hasOwnProperty("outerImage")&&(D.attr("src",d.outerImage),delete d.outerImage),bb.refresh()}var d=a.extend({},a.Jcrop.defaults),e,f=navigator.userAgent.toLowerCase(),g=/msie/.test(f),h=/msie [1-6]\./.test(f);typeof b!="object"&&(b=a(b)[0]),typeof c!="object"&&(c={}),n(c);var z={border:"none",visibility:"visible",margin:0,padding:0,position:"absolute",top:0,left:0},A=a(b),B=!0;if(b.tagName=="IMG"){if(A[0].width!=0&&A[0].height!=0)A.width(A[0].width),A.height(A[0].height);else{var C=new Image;C.src=A[0].src,A.width(C.width),A.height(C.height)}var D=A.clone().removeAttr("id"). -css(z).show();D.width(A.width()),D.height(A.height()),A.after(D).hide()}else D=A.css(z).show(),B=!1,d.shade===null&&(d.shade=!0);t(D,d.boxWidth,d.boxHeight);var E=D.width(),F=D.height(),G=a("
").width(E).height(F).addClass(j("holder")).css({position:"relative",backgroundColor:d.bgColor}).insertAfter(A).append(D);d.addClass&&G.addClass(d.addClass);var H=a("
"),I=a("
").width("100%").height("100%").css({zIndex:310,position:"absolute",overflow:"hidden"}),J=a("
").width("100%").height("100%").css("zIndex",320),K=a("
").css({position:"absolute",zIndex:600}).dblclick(function(){var a=_.getFixed();d.onDblClick.call(bs,a)}).insertBefore(D).append(I,J);B&&(H=a("").attr("src",D.attr("src")).css(z).width(E).height(F),I.append(H)),h&&K.css({overflowY:"hidden"});var L=d.boundary,M=y().width(E+L*2).height(F+L*2).css({position:"absolute",top:i(-L),left:i(-L),zIndex:290}).mousedown(w),N=d.bgColor,O=d.bgOpacity,P,Q,R,S,T,U,V=!0,W,X,Y;e=l(D);var Z=function(){function a(){var a={},b=["touchstart" -,"touchmove","touchend"],c=document.createElement("div"),d;try{for(d=0;da+f&&(f-=f+a),0>b+g&&(g-=g+b),FE&&(r=E,u=Math.abs((r-a)/f),s=k<0?b-u:u+b)):(r=c,u=l/f,s=k<0?b-u:b+u,s<0?(s=0,t=Math.abs((s-b)*f),r=j<0?a-t:t+a):s>F&&(s=F,t=Math.abs(s-b)*f,r=j<0?a-t:t+a)),r>a?(r-ah&&(r=a+h),s>b?s=b+(r-a)/f:s=b-(r-a)/f):rh&&(r=a-h),s>b?s=b+(a-r)/f:s=b-(a-r)/f),r<0?(a-=r,r=0):r>E&&(a-=r-E,r=E),s<0?(b-=s,s=0):s>F&&(b-=s-F,s=F),q(o(a,b,r,s))}function n(a){return a[0]<0&&(a[0]=0),a[1]<0&&(a[1]=0),a[0]>E&&(a[0]=E),a[1]>F&&(a[1]=F),[Math.round(a[0]),Math.round(a[1])]}function o(a,b,c,d){var e=a,f=c,g=b,h=d;return cP&&(c=d>0?a+P:a-P),Q&&Math.abs -(f)>Q&&(e=f>0?b+Q:b-Q),S/U&&Math.abs(f)0?b+S/U:b-S/U),R/T&&Math.abs(d)0?a+R/T:a-R/T),a<0&&(c-=a,a-=a),b<0&&(e-=b,b-=b),c<0&&(a-=c,c-=c),e<0&&(b-=e,e-=e),c>E&&(g=c-E,a-=g,c-=g),e>F&&(g=e-F,b-=g,e-=g),a>E&&(g=a-F,e-=g,b-=g),b>F&&(g=b-F,e-=g,b-=g),q(o(a,b,c,e))}function q(a){return{x:a[0],y:a[1],x2:a[2],y2:a[3],w:a[2]-a[0],h:a[3]-a[1]}}var a=0,b=0,c=0,e=0,f,g;return{flipCoords:o,setPressed:h,setCurrent:i,getOffset:j,moveOffset:k,getCorner:l,getFixed:m}}(),ba=function(){function f(a,b){e.left.css({height:i(b)}),e.right.css({height:i(b)})}function g(){return h(_.getFixed())}function h(a){e.top.css({left:i(a.x),width:i(a.w),height:i(a.y)}),e.bottom.css({top:i(a.y2),left:i(a.x),width:i(a.w),height:i(F-a.y2)}),e.right.css({left:i(a.x2),width:i(E-a.x2)}),e.left.css({width:i(a.x)})}function j(){return a("
").css({position:"absolute",backgroundColor:d.shadeColor||d.bgColor}).appendTo(c)}function k(){b||(b=!0,c.insertBefore(D),g(),bb.setBgOpacity(1,0,1),H.hide(),l(d.shadeColor||d.bgColor,1),bb. -isAwake()?n(d.bgOpacity,1):n(1,1))}function l(a,b){bq(p(),a,b)}function m(){b&&(c.remove(),H.show(),b=!1,bb.isAwake()?bb.setBgOpacity(d.bgOpacity,1,1):(bb.setBgOpacity(1,1,1),bb.disableHandles()),bq(G,0,1))}function n(a,e){b&&(d.bgFade&&!e?c.animate({opacity:1-a},{queue:!1,duration:d.fadeTime}):c.css({opacity:1-a}))}function o(){d.shade?k():m(),bb.isAwake()&&n(d.bgOpacity)}function p(){return c.children()}var b=!1,c=a("
").css({position:"absolute",zIndex:240,opacity:0}),e={top:j(),left:j().height(F),right:j().height(F),bottom:j()};return{update:g,updateRaw:h,getShades:p,setBgColor:l,enable:k,disable:m,resize:f,refresh:o,opacity:n}}(),bb=function(){function k(b){var c=a("
").css({position:"absolute",opacity:d.borderOpacity}).addClass(j(b));return I.append(c),c}function l(b,c){var d=a("
").mousedown(s(b)).css({cursor:b+"-resize",position:"absolute",zIndex:c}).addClass("ord-"+b);return Z.support&&d.bind("touchstart.jcrop",Z.createDragger(b)),J.append(d),d}function m(a){var b=d.handleSize,e=l(a,c++ -).css({opacity:d.handleOpacity}).addClass(j("handle"));return b&&e.width(b).height(b),e}function n(a){return l(a,c++).addClass("jcrop-dragbar")}function o(a){var b;for(b=0;b').css({position:"fixed",left:"-120px",width:"12px"}).addClass("jcrop-keymgr"),c=a("
").css({position:"absolute",overflow:"hidden"}).append(b);return d.keySupport&&(b.keydown(i).blur(f),h||!d.fixedSupport?(b.css({position:"absolute",left:"-20px"}),c.append(b).insertBefore(D)):b.insertBefore(D)),{watchKeys:e}}();Z.support&&M.bind("touchstart.jcrop",Z.newSelection),J.hide(),br(!0);var bs={setImage:bp,animateTo:bf,setSelect:bg,setOptions:bk,tellSelect:bi,tellScaled:bj,setClass:be,disable:bl,enable:bm,cancel:bn,release:bb.release,destroy:bo,focus:bd.watchKeys,getBounds:function(){return[E*T,F*U]},getWidgetSize:function(){return[E,F]},getScaleFactor:function(){return[T,U]},getOptions:function(){return d},ui:{holder:G,selection:K}};return g&&G.bind("selectstart",function(){return!1}),A.data("Jcrop",bs),bs},a.fn.Jcrop=function(b,c){var d;return this.each(function(){if(a(this).data("Jcrop")){if( -b==="api")return a(this).data("Jcrop");a(this).data("Jcrop").setOptions(b)}else this.tagName=="IMG"?a.Jcrop.Loader(this,function(){a(this).css({display:"block",visibility:"hidden"}),d=a.Jcrop(this,b),a.isFunction(c)&&c.call(d)}):(a(this).css({display:"block",visibility:"hidden"}),d=a.Jcrop(this,b),a.isFunction(c)&&c.call(d))}),this},a.Jcrop.Loader=function(b,c,d){function g(){f.complete?(e.unbind(".jcloader"),a.isFunction(c)&&c.call(f)):window.setTimeout(g,50)}var e=a(b),f=e[0];e.bind("load.jcloader",g).bind("error.jcloader",function(b){e.unbind(".jcloader"),a.isFunction(d)&&d.call(f)}),f.complete&&a.isFunction(c)&&(e.unbind(".jcloader"),c.call(f))},a.Jcrop.defaults={allowSelect:!0,allowMove:!0,allowResize:!0,trackDocument:!0,baseClass:"jcrop",addClass:null,bgColor:"black",bgOpacity:.6,bgFade:!1,borderOpacity:.4,handleOpacity:.5,handleSize:null,aspectRatio:0,keySupport:!0,createHandles:["n","s","e","w","nw","ne","se","sw"],createDragbars:["n","s","e","w"],createBorders:["n","s","e","w"],drawBorders:!0,dragEdges -:!0,fixedSupport:!0,touchSupport:null,shade:null,boxWidth:0,boxHeight:0,boundary:2,fadeTime:400,animationDelay:20,swingSpeed:3,minSelect:[0,0],maxSize:[0,0],minSize:[0,0],onChange:function(){},onSelect:function(){},onDblClick:function(){},onRelease:function(){}}})(jQuery); \ No newline at end of file diff -r 623836db99af -r 4556eebe5d73 light9/web/lib/tapmodo-Jcrop-1902fbc/js/jquery.color.js --- a/light9/web/lib/tapmodo-Jcrop-1902fbc/js/jquery.color.js Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,661 +0,0 @@ -/*! - * jQuery Color Animations v2.0pre - * http://jquery.org/ - * - * Copyright 2011 John Resig - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - */ - -(function( jQuery, undefined ){ - var stepHooks = "backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color outlineColor".split(" "), - - // plusequals test for += 100 -= 100 - rplusequals = /^([\-+])=\s*(\d+\.?\d*)/, - // a set of RE's that can match strings and generate color tuples. - stringParsers = [{ - re: /rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/, - parse: function( execResult ) { - return [ - execResult[ 1 ], - execResult[ 2 ], - execResult[ 3 ], - execResult[ 4 ] - ]; - } - }, { - re: /rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/, - parse: function( execResult ) { - return [ - 2.55 * execResult[1], - 2.55 * execResult[2], - 2.55 * execResult[3], - execResult[ 4 ] - ]; - } - }, { - re: /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/, - parse: function( execResult ) { - return [ - parseInt( execResult[ 1 ], 16 ), - parseInt( execResult[ 2 ], 16 ), - parseInt( execResult[ 3 ], 16 ) - ]; - } - }, { - re: /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/, - parse: function( execResult ) { - return [ - parseInt( execResult[ 1 ] + execResult[ 1 ], 16 ), - parseInt( execResult[ 2 ] + execResult[ 2 ], 16 ), - parseInt( execResult[ 3 ] + execResult[ 3 ], 16 ) - ]; - } - }, { - re: /hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/, - space: "hsla", - parse: function( execResult ) { - return [ - execResult[1], - execResult[2] / 100, - execResult[3] / 100, - execResult[4] - ]; - } - }], - - // jQuery.Color( ) - color = jQuery.Color = function( color, green, blue, alpha ) { - return new jQuery.Color.fn.parse( color, green, blue, alpha ); - }, - spaces = { - rgba: { - cache: "_rgba", - props: { - red: { - idx: 0, - type: "byte", - empty: true - }, - green: { - idx: 1, - type: "byte", - empty: true - }, - blue: { - idx: 2, - type: "byte", - empty: true - }, - alpha: { - idx: 3, - type: "percent", - def: 1 - } - } - }, - hsla: { - cache: "_hsla", - props: { - hue: { - idx: 0, - type: "degrees", - empty: true - }, - saturation: { - idx: 1, - type: "percent", - empty: true - }, - lightness: { - idx: 2, - type: "percent", - empty: true - } - } - } - }, - propTypes = { - "byte": { - floor: true, - min: 0, - max: 255 - }, - "percent": { - min: 0, - max: 1 - }, - "degrees": { - mod: 360, - floor: true - } - }, - rgbaspace = spaces.rgba.props, - support = color.support = {}, - - // colors = jQuery.Color.names - colors, - - // local aliases of functions called often - each = jQuery.each; - - spaces.hsla.props.alpha = rgbaspace.alpha; - - function clamp( value, prop, alwaysAllowEmpty ) { - var type = propTypes[ prop.type ] || {}, - allowEmpty = prop.empty || alwaysAllowEmpty; - - if ( allowEmpty && value == null ) { - return null; - } - if ( prop.def && value == null ) { - return prop.def; - } - if ( type.floor ) { - value = ~~value; - } else { - value = parseFloat( value ); - } - if ( value == null || isNaN( value ) ) { - return prop.def; - } - if ( type.mod ) { - value = value % type.mod; - // -10 -> 350 - return value < 0 ? type.mod + value : value; - } - - // for now all property types without mod have min and max - return type.min > value ? type.min : type.max < value ? type.max : value; - } - - function stringParse( string ) { - var inst = color(), - rgba = inst._rgba = []; - - string = string.toLowerCase(); - - each( stringParsers, function( i, parser ) { - var match = parser.re.exec( string ), - values = match && parser.parse( match ), - parsed, - spaceName = parser.space || "rgba", - cache = spaces[ spaceName ].cache; - - - if ( values ) { - parsed = inst[ spaceName ]( values ); - - // if this was an rgba parse the assignment might happen twice - // oh well.... - inst[ cache ] = parsed[ cache ]; - rgba = inst._rgba = parsed._rgba; - - // exit each( stringParsers ) here because we matched - return false; - } - }); - - // Found a stringParser that handled it - if ( rgba.length !== 0 ) { - - // if this came from a parsed string, force "transparent" when alpha is 0 - // chrome, (and maybe others) return "transparent" as rgba(0,0,0,0) - if ( Math.max.apply( Math, rgba ) === 0 ) { - jQuery.extend( rgba, colors.transparent ); - } - return inst; - } - - // named colors / default - filter back through parse function - if ( string = colors[ string ] ) { - return string; - } - } - - color.fn = color.prototype = { - constructor: color, - parse: function( red, green, blue, alpha ) { - if ( red === undefined ) { - this._rgba = [ null, null, null, null ]; - return this; - } - if ( red instanceof jQuery || red.nodeType ) { - red = red instanceof jQuery ? red.css( green ) : jQuery( red ).css( green ); - green = undefined; - } - - var inst = this, - type = jQuery.type( red ), - rgba = this._rgba = [], - source; - - // more than 1 argument specified - assume ( red, green, blue, alpha ) - if ( green !== undefined ) { - red = [ red, green, blue, alpha ]; - type = "array"; - } - - if ( type === "string" ) { - return this.parse( stringParse( red ) || colors._default ); - } - - if ( type === "array" ) { - each( rgbaspace, function( key, prop ) { - rgba[ prop.idx ] = clamp( red[ prop.idx ], prop ); - }); - return this; - } - - if ( type === "object" ) { - if ( red instanceof color ) { - each( spaces, function( spaceName, space ) { - if ( red[ space.cache ] ) { - inst[ space.cache ] = red[ space.cache ].slice(); - } - }); - } else { - each( spaces, function( spaceName, space ) { - each( space.props, function( key, prop ) { - var cache = space.cache; - - // if the cache doesn't exist, and we know how to convert - if ( !inst[ cache ] && space.to ) { - - // if the value was null, we don't need to copy it - // if the key was alpha, we don't need to copy it either - if ( red[ key ] == null || key === "alpha") { - return; - } - inst[ cache ] = space.to( inst._rgba ); - } - - // this is the only case where we allow nulls for ALL properties. - // call clamp with alwaysAllowEmpty - inst[ cache ][ prop.idx ] = clamp( red[ key ], prop, true ); - }); - }); - } - return this; - } - }, - is: function( compare ) { - var is = color( compare ), - same = true, - myself = this; - - each( spaces, function( _, space ) { - var isCache = is[ space.cache ], - localCache; - if (isCache) { - localCache = myself[ space.cache ] || space.to && space.to( myself._rgba ) || []; - each( space.props, function( _, prop ) { - if ( isCache[ prop.idx ] != null ) { - same = ( isCache[ prop.idx ] === localCache[ prop.idx ] ); - return same; - } - }); - } - return same; - }); - return same; - }, - _space: function() { - var used = [], - inst = this; - each( spaces, function( spaceName, space ) { - if ( inst[ space.cache ] ) { - used.push( spaceName ); - } - }); - return used.pop(); - }, - transition: function( other, distance ) { - var end = color( other ), - spaceName = end._space(), - space = spaces[ spaceName ], - start = this[ space.cache ] || space.to( this._rgba ), - result = start.slice(); - - end = end[ space.cache ]; - each( space.props, function( key, prop ) { - var index = prop.idx, - startValue = start[ index ], - endValue = end[ index ], - type = propTypes[ prop.type ] || {}; - - // if null, don't override start value - if ( endValue === null ) { - return; - } - // if null - use end - if ( startValue === null ) { - result[ index ] = endValue; - } else { - if ( type.mod ) { - if ( endValue - startValue > type.mod / 2 ) { - startValue += type.mod; - } else if ( startValue - endValue > type.mod / 2 ) { - startValue -= type.mod; - } - } - result[ prop.idx ] = clamp( ( endValue - startValue ) * distance + startValue, prop ); - } - }); - return this[ spaceName ]( result ); - }, - blend: function( opaque ) { - // if we are already opaque - return ourself - if ( this._rgba[ 3 ] === 1 ) { - return this; - } - - var rgb = this._rgba.slice(), - a = rgb.pop(), - blend = color( opaque )._rgba; - - return color( jQuery.map( rgb, function( v, i ) { - return ( 1 - a ) * blend[ i ] + a * v; - })); - }, - toRgbaString: function() { - var prefix = "rgba(", - rgba = jQuery.map( this._rgba, function( v, i ) { - return v == null ? ( i > 2 ? 1 : 0 ) : v; - }); - - if ( rgba[ 3 ] === 1 ) { - rgba.pop(); - prefix = "rgb("; - } - - return prefix + rgba.join(",") + ")"; - }, - toHslaString: function() { - var prefix = "hsla(", - hsla = jQuery.map( this.hsla(), function( v, i ) { - if ( v == null ) { - v = i > 2 ? 1 : 0; - } - - // catch 1 and 2 - if ( i && i < 3 ) { - v = Math.round( v * 100 ) + "%"; - } - return v; - }); - - if ( hsla[ 3 ] === 1 ) { - hsla.pop(); - prefix = "hsl("; - } - return prefix + hsla.join(",") + ")"; - }, - toHexString: function( includeAlpha ) { - var rgba = this._rgba.slice(), - alpha = rgba.pop(); - - if ( includeAlpha ) { - rgba.push( ~~( alpha * 255 ) ); - } - - return "#" + jQuery.map( rgba, function( v, i ) { - - // default to 0 when nulls exist - v = ( v || 0 ).toString( 16 ); - return v.length === 1 ? "0" + v : v; - }).join(""); - }, - toString: function() { - return this._rgba[ 3 ] === 0 ? "transparent" : this.toRgbaString(); - } - }; - color.fn.parse.prototype = color.fn; - - // hsla conversions adapted from: - // http://www.google.com/codesearch/p#OAMlx_jo-ck/src/third_party/WebKit/Source/WebCore/inspector/front-end/Color.js&d=7&l=193 - - function hue2rgb( p, q, h ) { - h = ( h + 1 ) % 1; - if ( h * 6 < 1 ) { - return p + (q - p) * 6 * h; - } - if ( h * 2 < 1) { - return q; - } - if ( h * 3 < 2 ) { - return p + (q - p) * ((2/3) - h) * 6; - } - return p; - } - - spaces.hsla.to = function ( rgba ) { - if ( rgba[ 0 ] == null || rgba[ 1 ] == null || rgba[ 2 ] == null ) { - return [ null, null, null, rgba[ 3 ] ]; - } - var r = rgba[ 0 ] / 255, - g = rgba[ 1 ] / 255, - b = rgba[ 2 ] / 255, - a = rgba[ 3 ], - max = Math.max( r, g, b ), - min = Math.min( r, g, b ), - diff = max - min, - add = max + min, - l = add * 0.5, - h, s; - - if ( min === max ) { - h = 0; - } else if ( r === max ) { - h = ( 60 * ( g - b ) / diff ) + 360; - } else if ( g === max ) { - h = ( 60 * ( b - r ) / diff ) + 120; - } else { - h = ( 60 * ( r - g ) / diff ) + 240; - } - - if ( l === 0 || l === 1 ) { - s = l; - } else if ( l <= 0.5 ) { - s = diff / add; - } else { - s = diff / ( 2 - add ); - } - return [ Math.round(h) % 360, s, l, a == null ? 1 : a ]; - }; - - spaces.hsla.from = function ( hsla ) { - if ( hsla[ 0 ] == null || hsla[ 1 ] == null || hsla[ 2 ] == null ) { - return [ null, null, null, hsla[ 3 ] ]; - } - var h = hsla[ 0 ] / 360, - s = hsla[ 1 ], - l = hsla[ 2 ], - a = hsla[ 3 ], - q = l <= 0.5 ? l * ( 1 + s ) : l + s - l * s, - p = 2 * l - q, - r, g, b; - - return [ - Math.round( hue2rgb( p, q, h + ( 1 / 3 ) ) * 255 ), - Math.round( hue2rgb( p, q, h ) * 255 ), - Math.round( hue2rgb( p, q, h - ( 1 / 3 ) ) * 255 ), - a - ]; - }; - - - each( spaces, function( spaceName, space ) { - var props = space.props, - cache = space.cache, - to = space.to, - from = space.from; - - // makes rgba() and hsla() - color.fn[ spaceName ] = function( value ) { - - // generate a cache for this space if it doesn't exist - if ( to && !this[ cache ] ) { - this[ cache ] = to( this._rgba ); - } - if ( value === undefined ) { - return this[ cache ].slice(); - } - - var type = jQuery.type( value ), - arr = ( type === "array" || type === "object" ) ? value : arguments, - local = this[ cache ].slice(), - ret; - - each( props, function( key, prop ) { - var val = arr[ type === "object" ? key : prop.idx ]; - if ( val == null ) { - val = local[ prop.idx ]; - } - local[ prop.idx ] = clamp( val, prop ); - }); - - if ( from ) { - ret = color( from( local ) ); - ret[ cache ] = local; - return ret; - } else { - return color( local ); - } - }; - - // makes red() green() blue() alpha() hue() saturation() lightness() - each( props, function( key, prop ) { - // alpha is included in more than one space - if ( color.fn[ key ] ) { - return; - } - color.fn[ key ] = function( value ) { - var vtype = jQuery.type( value ), - fn = ( key === 'alpha' ? ( this._hsla ? 'hsla' : 'rgba' ) : spaceName ), - local = this[ fn ](), - cur = local[ prop.idx ], - match; - - if ( vtype === "undefined" ) { - return cur; - } - - if ( vtype === "function" ) { - value = value.call( this, cur ); - vtype = jQuery.type( value ); - } - if ( value == null && prop.empty ) { - return this; - } - if ( vtype === "string" ) { - match = rplusequals.exec( value ); - if ( match ) { - value = cur + parseFloat( match[ 2 ] ) * ( match[ 1 ] === "+" ? 1 : -1 ); - } - } - local[ prop.idx ] = value; - return this[ fn ]( local ); - }; - }); - }); - - // add .fx.step functions - each( stepHooks, function( i, hook ) { - jQuery.cssHooks[ hook ] = { - set: function( elem, value ) { - var parsed, backgroundColor, curElem; - - if ( jQuery.type( value ) !== 'string' || ( parsed = stringParse( value ) ) ) - { - value = color( parsed || value ); - if ( !support.rgba && value._rgba[ 3 ] !== 1 ) { - curElem = hook === "backgroundColor" ? elem.parentNode : elem; - do { - backgroundColor = jQuery.curCSS( curElem, "backgroundColor" ); - } while ( - ( backgroundColor === "" || backgroundColor === "transparent" ) && - ( curElem = curElem.parentNode ) && - curElem.style - ); - - value = value.blend( backgroundColor && backgroundColor !== "transparent" ? - backgroundColor : - "_default" ); - } - - value = value.toRgbaString(); - } - elem.style[ hook ] = value; - } - }; - jQuery.fx.step[ hook ] = function( fx ) { - if ( !fx.colorInit ) { - fx.start = color( fx.elem, hook ); - fx.end = color( fx.end ); - fx.colorInit = true; - } - jQuery.cssHooks[ hook ].set( fx.elem, fx.start.transition( fx.end, fx.pos ) ); - }; - }); - - // detect rgba support - jQuery(function() { - var div = document.createElement( "div" ), - div_style = div.style; - - div_style.cssText = "background-color:rgba(1,1,1,.5)"; - support.rgba = div_style.backgroundColor.indexOf( "rgba" ) > -1; - }); - - // Some named colors to work with - // From Interface by Stefan Petre - // http://interface.eyecon.ro/ - colors = jQuery.Color.names = { - aqua: "#00ffff", - azure: "#f0ffff", - beige: "#f5f5dc", - black: "#000000", - blue: "#0000ff", - brown: "#a52a2a", - cyan: "#00ffff", - darkblue: "#00008b", - darkcyan: "#008b8b", - darkgrey: "#a9a9a9", - darkgreen: "#006400", - darkkhaki: "#bdb76b", - darkmagenta: "#8b008b", - darkolivegreen: "#556b2f", - darkorange: "#ff8c00", - darkorchid: "#9932cc", - darkred: "#8b0000", - darksalmon: "#e9967a", - darkviolet: "#9400d3", - fuchsia: "#ff00ff", - gold: "#ffd700", - green: "#008000", - indigo: "#4b0082", - khaki: "#f0e68c", - lightblue: "#add8e6", - lightcyan: "#e0ffff", - lightgreen: "#90ee90", - lightgrey: "#d3d3d3", - lightpink: "#ffb6c1", - lightyellow: "#ffffe0", - lime: "#00ff00", - magenta: "#ff00ff", - maroon: "#800000", - navy: "#000080", - olive: "#808000", - orange: "#ffa500", - pink: "#ffc0cb", - purple: "#800080", - violet: "#800080", - red: "#ff0000", - silver: "#c0c0c0", - white: "#ffffff", - yellow: "#ffff00", - transparent: [ null, null, null, 0 ], - _default: "#ffffff" - }; -})( jQuery ); diff -r 623836db99af -r 4556eebe5d73 light9/web/lib/tapmodo-Jcrop-1902fbc/js/jquery.min.js --- a/light9/web/lib/tapmodo-Jcrop-1902fbc/js/jquery.min.js Thu Jun 08 15:05:59 2023 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ -/*! jQuery v1.9.0 | (c) 2005, 2012 jQuery Foundation, Inc. | jquery.org/license */(function(e,t){"use strict";function n(e){var t=e.length,n=st.type(e);return st.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}function r(e){var t=Tt[e]={};return st.each(e.match(lt)||[],function(e,n){t[n]=!0}),t}function i(e,n,r,i){if(st.acceptData(e)){var o,a,s=st.expando,u="string"==typeof n,l=e.nodeType,c=l?st.cache:e,f=l?e[s]:e[s]&&s;if(f&&c[f]&&(i||c[f].data)||!u||r!==t)return f||(l?e[s]=f=K.pop()||st.guid++:f=s),c[f]||(c[f]={},l||(c[f].toJSON=st.noop)),("object"==typeof n||"function"==typeof n)&&(i?c[f]=st.extend(c[f],n):c[f].data=st.extend(c[f].data,n)),o=c[f],i||(o.data||(o.data={}),o=o.data),r!==t&&(o[st.camelCase(n)]=r),u?(a=o[n],null==a&&(a=o[st.camelCase(n)])):a=o,a}}function o(e,t,n){if(st.acceptData(e)){var r,i,o,a=e.nodeType,u=a?st.cache:e,l=a?e[st.expando]:st.expando;if(u[l]){if(t&&(r=n?u[l]:u[l].data)){st.isArray(t)?t=t.concat(st.map(t,st.camelCase)):t in r?t=[t]:(t=st.camelCase(t),t=t in r?[t]:t.split(" "));for(i=0,o=t.length;o>i;i++)delete r[t[i]];if(!(n?s:st.isEmptyObject)(r))return}(n||(delete u[l].data,s(u[l])))&&(a?st.cleanData([e],!0):st.support.deleteExpando||u!=u.window?delete u[l]:u[l]=null)}}}function a(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(Nt,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:wt.test(r)?st.parseJSON(r):r}catch(o){}st.data(e,n,r)}else r=t}return r}function s(e){var t;for(t in e)if(("data"!==t||!st.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function u(){return!0}function l(){return!1}function c(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function f(e,t,n){if(t=t||0,st.isFunction(t))return st.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return st.grep(e,function(e){return e===t===n});if("string"==typeof t){var r=st.grep(e,function(e){return 1===e.nodeType});if(Wt.test(t))return st.filter(t,r,!n);t=st.filter(t,r)}return st.grep(e,function(e){return st.inArray(e,t)>=0===n})}function p(e){var t=zt.split("|"),n=e.createDocumentFragment();if(n.createElement)for(;t.length;)n.createElement(t.pop());return n}function d(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function h(e){var t=e.getAttributeNode("type");return e.type=(t&&t.specified)+"/"+e.type,e}function g(e){var t=nn.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function m(e,t){for(var n,r=0;null!=(n=e[r]);r++)st._data(n,"globalEval",!t||st._data(t[r],"globalEval"))}function y(e,t){if(1===t.nodeType&&st.hasData(e)){var n,r,i,o=st._data(e),a=st._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)st.event.add(t,n,s[n][r])}a.data&&(a.data=st.extend({},a.data))}}function v(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!st.support.noCloneEvent&&t[st.expando]){r=st._data(t);for(i in r.events)st.removeEvent(t,i,r.handle);t.removeAttribute(st.expando)}"script"===n&&t.text!==e.text?(h(t).text=e.text,g(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),st.support.html5Clone&&e.innerHTML&&!st.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Zt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}function b(e,n){var r,i,o=0,a=e.getElementsByTagName!==t?e.getElementsByTagName(n||"*"):e.querySelectorAll!==t?e.querySelectorAll(n||"*"):t;if(!a)for(a=[],r=e.childNodes||e;null!=(i=r[o]);o++)!n||st.nodeName(i,n)?a.push(i):st.merge(a,b(i,n));return n===t||n&&st.nodeName(e,n)?st.merge([e],a):a}function x(e){Zt.test(e.type)&&(e.defaultChecked=e.checked)}function T(e,t){if(t in e)return t;for(var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=Nn.length;i--;)if(t=Nn[i]+n,t in e)return t;return r}function w(e,t){return e=t||e,"none"===st.css(e,"display")||!st.contains(e.ownerDocument,e)}function N(e,t){for(var n,r=[],i=0,o=e.length;o>i;i++)n=e[i],n.style&&(r[i]=st._data(n,"olddisplay"),t?(r[i]||"none"!==n.style.display||(n.style.display=""),""===n.style.display&&w(n)&&(r[i]=st._data(n,"olddisplay",S(n.nodeName)))):r[i]||w(n)||st._data(n,"olddisplay",st.css(n,"display")));for(i=0;o>i;i++)n=e[i],n.style&&(t&&"none"!==n.style.display&&""!==n.style.display||(n.style.display=t?r[i]||"":"none"));return e}function C(e,t,n){var r=mn.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function k(e,t,n,r,i){for(var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;4>o;o+=2)"margin"===n&&(a+=st.css(e,n+wn[o],!0,i)),r?("content"===n&&(a-=st.css(e,"padding"+wn[o],!0,i)),"margin"!==n&&(a-=st.css(e,"border"+wn[o]+"Width",!0,i))):(a+=st.css(e,"padding"+wn[o],!0,i),"padding"!==n&&(a+=st.css(e,"border"+wn[o]+"Width",!0,i)));return a}function E(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=ln(e),a=st.support.boxSizing&&"border-box"===st.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=un(e,t,o),(0>i||null==i)&&(i=e.style[t]),yn.test(i))return i;r=a&&(st.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+k(e,t,n||(a?"border":"content"),r,o)+"px"}function S(e){var t=V,n=bn[e];return n||(n=A(e,t),"none"!==n&&n||(cn=(cn||st("