changeset 1795:499d5c6b153b

rewrite /live to edit effects in-graph Ignore-this: ea45128f7bae6d17fda122991105eed
author Drew Perttula <drewp@bigasterisk.com>
date Thu, 07 Jun 2018 23:04:50 +0000
parents c97c0ac03597
children a5f44483e4c5
files light9/web/live/elements.html light9/web/live/live.coffee
diffstat 2 files changed, 148 insertions(+), 115 deletions(-) [+]
line wrap: on
line diff
--- a/light9/web/live/elements.html	Thu Jun 07 23:04:26 2018 +0000
+++ b/light9/web/live/elements.html	Thu Jun 07 23:04:50 2018 +0000
@@ -224,16 +224,14 @@
     </style>
     <rdfdb-synced-graph graph="{{graph}}"></rdfdb-synced-graph>
 
-    <h1>device control
-      <button on-click="clearAll">clear all</button></h1>
+    <h1>device control</h1>
 
     <div id="save">
       <div>
-        effect name: <input type="input" value="{{newEffectName::change}}">
-        <button on-click="saveNewEffect">save new effect</button>
-        <edit-choice graph="{{graph}}" uri="{{effect}}"></edit-choice>
+        <button on-click="newEffect">New effect</button>
+        <edit-choice graph="{{graph}}" uri="{{effectChoice}}"></edit-choice>
+        <button on-click="clearAll">clear settings in this effect</button>
       </div>
-      <textarea id="preview" value="{{effectPreview}}"></textarea>
     </div>
 
     <div id="deviceControls">
@@ -246,7 +244,6 @@
         ></light9-live-device-control>
       </template>
     </div>
-
     
   </template>
 </dom-module>
--- a/light9/web/live/live.coffee	Thu Jun 07 23:04:26 2018 +0000
+++ b/light9/web/live/live.coffee	Thu Jun 07 23:04:50 2018 +0000
@@ -1,5 +1,26 @@
 log = console.log
 
+# Like element.set(path, newArray), but minimizes splices.
+# Dotted paths don't work yet.
+syncArray = (element, path, newArray, isElementEqual) ->
+  pos = 0
+  newPos = 0
+
+  while newPos < newArray.length
+    if pos < element[path].length
+      if isElementEqual(element[path][pos], newArray[newPos])
+        pos += 1
+        newPos += 1
+      else
+        element.splice('devices', pos, 1)
+    else
+      element.push('devices', newArray[newPos])
+      pos += 1
+      newPos += 1
+
+  if pos < element[path].length
+    element.splice('devices', pos, element[path].length - pos)
+
 valuePred = (graph, attr) ->
   U = (x) -> graph.Uri(x)
   scaledAttributeTypes = [U(':color'), U(':brightness'), U(':uv')]
@@ -35,12 +56,18 @@
   graphValueChanged: (v) ->
     log('change: control gets', v)
     @enableChange = false
+    if v == null
+      if @deviceAttrRow.useColor
+        v = '#000000'
+      else
+        v = 0
     @value = v
     @sliderWriteValue = v if @deviceAttrRow.useSlider
     @enableChange = true
+    
   onChange: (value) ->
     return unless @graphToControls? and @enableChange
-    log('change: control tells graph', @deviceAttrRow.uri.value, value)
+    #log('change: control tells graph', @deviceAttrRow.uri.value, value)
     @graphToControls.controlChanged(@device, @deviceAttrRow.uri, value)
 
   clear: ->
@@ -120,73 +147,108 @@
   # as opposed to letting each widget scan the graph and push lots of
   # tiny patches to it.
   constructor: (@graph) ->
-    @currentSettings = {} # {dev: {attr: value}}
+
+    # Registered graphValueChanged funcs, by dev+attr
     @onChanged = {}
-    @effect = @graph.Uri('http://light9.bigasterisk.com/effect/pool_r')
-    @ctx = @graph.Uri('http://light9.bigasterisk.com/show/dance2017/effect/pool_r')
+
+    # The settings we're showing (or would like to but the widget
+    # isn't registered yet):
+    # dev+attr : {setting: Uri, onChangeFunc: f, jsValue: str_or_float}
+    @settings = new Map()
+
+    @effect = null
+
+  ctxForEffect: (effect) ->
+    @graph.Uri(effect.value.replace(
+      "light9.bigasterisk.com/effect",
+      "light9.bigasterisk.com/show/dance2017/effect"))
+      
+  setEffect: (effect) ->
+    @clearSettings()
+    @effect = effect
+    @ctx = @ctxForEffect(@effect)
+    # are these going to pile up? consider @graph.triggerHandler('GTC sync')
     @graph.runHandler(@syncFromGraph.bind(@), 'GraphToControls sync')
 
+  newEffect: ->
+    # wrong- this should be our editor's scratch effect, promoted to a
+    # real one when you name it.
+    U = (x) => @graph.Uri(x)
+    effect = @graph.nextNumberedResource(U('http://light9.bigasterisk.com/effect/effect'))
+    ctx = @ctxForEffect(effect)
+    quad = (s, p, o) => @graph.Quad(s, p, o, ctx)
+
+    addQuads = [
+      quad(effect, U('rdf:type'), U(':Effect'))
+      quad(effect, U('rdfs:label'), @graph.Literal(effect.value.replace(/.*\//, "")))
+      quad(effect, U(':publishAttr'), U(':strength'))
+    ]
+    patch = {addQuads: addQuads, delQuads: []}
+    log('init new effect', patch)
+    @graph.applyAndSendPatch(patch)
+    return effect
+
+  addSettingsRow: (device, deviceAttr, setting, value) ->
+    key = device.value + " " + deviceAttr.value
+    @settings.set(key, {
+      setting: setting,
+      onChangeFunc: @onChanged[key],
+      jsValue: value
+    })
+                                                                              
   syncFromGraph: ->
     U = (x) => @graph.Uri(x)
-    @currentSettings = {}
     return if not @effect
-    for s in @graph.objects(@effect, U(':setting'))
-      dev = @graph.uriValue(s, U(':device'))
-      devAttr = @graph.uriValue(s, U(':deviceAttr'))
+    
+    toClear = new Map(@settings)
+    
+    for setting in @graph.objects(@effect, U(':setting'))
+      dev = @graph.uriValue(setting, U(':device'))
+      devAttr = @graph.uriValue(setting, U(':deviceAttr'))
+      key = dev.value + " " + devAttr.value
 
       pred = valuePred(@graph, devAttr)
       try
-        value = @graph.floatValue(s, pred)
+        value = @graph.floatValue(setting, pred)
       catch
-        value = @graph.stringValue(s, pred)
+        value = @graph.stringValue(setting, pred)
       log('change: graph contains', devAttr, value)
 
-      oc = @onChanged[dev.value + " " + devAttr.value]
-      if oc?
-        log('change: gtc tells control')
-        oc(value)
+      if @settings.has(key)
+        @settings.get(key).jsValue = value
+        @settings.get(key).onChangeFunc(value)
+        toClear.delete(key)
+      else
+        @addSettingsRow(dev, devAttr, setting, value)
+        if @onChanged[key]?
+          @onChanged[key](value)
+          
+    for key, row of toClear
+      row.onChangeFunc(null)      
+      @settings.delete(key)
 
-      # currentSettings is no longer what we need- the point is to
-      # optimize effectSettingLookup by knowing the :setting nodes for
-      # every dev+devAttr
-                  
-      # this is a bug for zoom=0, since collector will default it to
-      # stick at the last setting if we don't explicitly send the
-      # 0. rx/ry similar though not the exact same deal because of
-      # their remap.
-      if value == 0 or value == '#000000' or value == null or value == undefined
-        delete @currentSettings[dev.value][devAttr.value] if @currentSettings[dev.value]?
-      else
-        @currentSettings[dev.value] = {} unless @currentSettings[dev.value]?
-        @currentSettings[dev.value][devAttr.value] = value
+  clearSettings: ->
+    @settings.forEach (row, key) =>
+      row.onChangeFunc(null) if row.onChangeFunc?
+
+    @settings.clear()
 
   effectSettingLookup: (device, attr) ->
-    U = (x) => @graph.Uri(x)
-    for s in @graph.objects(@effect, U(':setting'))
-      if @graph.uriValue(s, U(':device')).equals(device) and @graph.uriValue(s, U(':deviceAttr')).equals(attr)
-        return s
+    key = device.value + " " + attr.value
+    if @settings.has(key)
+      return @settings.get(key).setting
+
     return null
 
-    # faster one:
-    d = @currentSettings[device.value]
-    if d?
-      da = d[attr.value]
-      # ...
-    return null
-      
-  preview: ->
-    JSON.stringify(@currentSettings)
+  register: (device, deviceAttr, graphValueChanged) ->
+    key = device.value + " " + deviceAttr.value
+
+    @onChanged[key] = graphValueChanged
 
-  register: (device, deviceAttr, graphValueChanged) ->
-    log('change: registering', device, deviceAttr)
-    @onChanged[device.value + " " + deviceAttr.value] = graphValueChanged
-    da = @currentSettings[device.value]
-    if da?
-      v = da[deviceAttr.value]
-      if v?
-        log('change: gtc tells change at reg time', v)
-        graphValueChanged(v)
-    # no unregister yet
+    if @settings.has(key)
+      row = @settings.get(key)
+      row.onChangeFunc = graphValueChanged
+      row.onChangeFunc(row.jsValue)
 
   shouldBeStored: (deviceAttr, value) ->
     # this is a bug for zoom=0, since collector will default it to
@@ -195,7 +257,14 @@
     # their remap.
     return value != 0 and value != '#000000'
 
+  emptyEffect: ->
+    new Map(@settings).forEach (row, key) =>
+      row.onChangeFunc(null)
+      @_removeEffectSetting(row.setting)
+
   controlChanged: (device, deviceAttr, value) ->
+    if not value? or (typeof value == "number" and isNaN(value))
+      throw new Error("controlChanged sent bad value " + value)
     effectSetting = @effectSettingLookup(device, deviceAttr)
     if @shouldBeStored(deviceAttr, value)
       if not effectSetting?
@@ -209,6 +278,7 @@
     U = (x) => @graph.Uri(x)
     quad = (s, p, o) => @graph.Quad(s, p, o, @ctx)
     effectSetting = @graph.nextNumberedResource(@effect.value + '_set')
+    @addSettingsRow(device, deviceAttr, effectSetting, value)
     addQuads = [
       quad(@effect, U(':setting'), effectSetting),
       quad(effectSetting, U(':device'),  device),
@@ -225,6 +295,7 @@
 
   _removeEffectSetting: (effectSetting) ->
     U = (x) => @graph.Uri(x)
+    quad = (s, p, o) => @graph.Quad(s, p, o, @ctx)
     if effectSetting?
       toDel = [quad(@effect, U(':setting'), effectSetting, @ctx)]
       for q in @graph.graph.getQuads(effectSetting)
@@ -236,93 +307,58 @@
   @is: "light9-live-controls"
   @getter_properties:
     graph: { type: Object, notify: true }
-    devices: { type: Array, notify: true }
-    effectPreview: { type: String, notify: true }
-    newEffectName: { type: String, notify: true }
-    effect: { type: String, notify: true } # the one being edited, if any
+    devices: { type: Array, notify: true, value: [] }
+    # string uri of the effect being edited, or null. This is the
+    # master value; GraphToControls follows.
+    effectChoice: { type: String, notify: true, value: null }
     graphToControls: { type: Object }
   @getter_observers: [
     'onGraph(graph)'
-    'onEffect(effect)'
+    'onEffectChoice(effectChoice)'
     ]
 
   constructor: ->
     super()
     @graphToControls = null
+
   ready: ->
     super.ready()
     @currentSettings = {}
-    @effectPreview = JSON.stringify({})
 
   onGraph: ->
     @graphToControls = new GraphToControls(@graph)
-    @effect = @graphToControls.effect.value
-    log('set my @graphtocontrols')
     @graph.runHandler(@update.bind(@), 'Light9LiveControls update')
 
   effectSettingLookup: (device, attr) ->
     if @graphToControls == null
       throw new Error('not ready')
-    # optimization for getting the :setting node
     return @graphToControls.effectSettingLookup(device, attr)
-      
-  onEffect: ->
-    U = (x) => @graph.Uri(x)
-    return unless @effect
-    log('load', @effect)
-    for s in @graph.objects(@effect, U(':setting'))
-      dev = @graph.uriValue(s, U(':device'))
-      devAttr = @graph.uriValue(s, U(':deviceAttr'))
-
-      pred = valuePred(@graph, devAttr)
-      try
-        value = @graph.floatValue(s, pred)
-      catch
-        value = @graph.stringValue(s, pred)
-      log('got', devAttr, value)
-      window.gather([[dev, devAttr, value]])
-      # there's nothing here to set the widgets to these values.
-            
-  saveNewEffect: ->
-    uriName = @newEffectName.replace(/[^a-zA-Z0-9_]/g, '')
-    return if not uriName.length
-
-    U = (x) => @graph.Uri(x)
 
-    @effect = U(U(":effect").value + "/#{uriName}")
-    ctx = U("http://light9.bigasterisk.com/show/dance2017/effect/#{uriName}")
-    quad = (s, p, o) => @graph.Quad(s, p, o, ctx)
-
-    addQuads = [
-      quad(@effect, U('rdf:type'), U(':Effect'))
-      quad(@effect, U('rdfs:label'), @graph.Literal(@newEffectName))
-      quad(@effect, U(':publishAttr'), U(':strength'))
-      ]
-    settings = @graph.nextNumberedResources(@effect.value + '_set', @currentSettingsList().length)
-    for row in @currentSettingsList()
-      setting = settings.shift()
-      addQuads.push(quad(@effect, U(':setting'), setting))
-      addQuads.push(quad(setting, U(':device'), row[0]))
-      addQuads.push(quad(setting, U(':deviceAttr'), row[1]))
-   
-      addQuads.push(quad(setting, valuePred(@graph, row[1]), value))
+  newEffect: ->
+    @effectChoice = @graphToControls.newEffect().value
       
-    patch = {addQuads: addQuads, delQuads: []}
-    log('save', patch)
-    @graph.applyAndSendPatch(patch)
-    @newEffectName = ''
-    
+  onEffectChoice: ->
+    U = (x) => @graph.Uri(x)
+    if not @effectChoice?
+      # unlink
+      @graphToControls.setEffect(null) if @graphToControls?
+    else
+      log('load', @effectChoice)
+      @graphToControls.setEffect(@graph.Uri(@effectChoice)) if @graphToControls?
+ 
   clearAll: ->
-    for dc in @shadowRoot.querySelectorAll("light9-live-device-control")
-      dc.clear()
+    # clears the effect!
+    @graphToControls.emptyEffect()
     
   update: ->
     U = (x) => @graph.Uri(x)
 
-    @set('devices', [])
+    newDevs = []
     for dc in @graph.sortedUris(@graph.subjects(U('rdf:type'), U(':DeviceClass')))
       for dev in @graph.sortedUris(@graph.subjects(U('rdf:type'), dc))
-        @push('devices', {uri: dev})
+        newDevs.push({uri: dev})
+
+    syncArray(@, 'devices', newDevs, (a, b) -> a.uri.value == b.uri.value)
 
     return