changeset 1788:8b9b8584ee42

rewrite /live to edit a real effect, not push its own values to collector Ignore-this: 295983a53720204b5cd12524190959f5
author Drew Perttula <drewp@bigasterisk.com>
date Thu, 07 Jun 2018 17:40:29 +0000
parents 4bd88d5fcaf8
children d8ae388a8076
files light9/web/live/elements.html light9/web/live/live.coffee
diffstat 2 files changed, 215 insertions(+), 85 deletions(-) [+]
line wrap: on
line diff
--- a/light9/web/live/elements.html	Thu Jun 07 17:39:41 2018 +0000
+++ b/light9/web/live/elements.html	Thu Jun 07 17:40:29 2018 +0000
@@ -4,7 +4,6 @@
 <link rel="import" href="/lib/paper-listbox/paper-listbox.html">
 <link rel="import" href="/lib/paper-item/paper-item.html">
 <link rel="import" href="/lib/iron-ajax/iron-ajax.html">
-<link rel="import" href="../light9-collector-client.html">
 
 <link rel="import" href="../rdfdb-synced-graph.html">
 <link rel="import" href="../resource-display.html">
@@ -113,24 +112,24 @@
      
     </style>
 
-    <template is="dom-if" if="{{deviceAttr.useSlider}}">
+    <template is="dom-if" if="{{deviceAttrRow.useSlider}}">
       <paper-slider min="0"
-                    max="{{deviceAttr.max}}"
+                    max="{{deviceAttrRow.max}}"
                     step=".001"
                     editable
                     content-type="application/json"
                     value="{{sliderWriteValue}}"
                     immediate-value="{{immediateSlider}}"></paper-slider>
     </template>
-    <template is="dom-if" if="{{deviceAttr.useColor}}">
+    <template is="dom-if" if="{{deviceAttrRow.useColor}}">
       <div id="colorControls">
         <button on-click="goBlack">0.0</button>
         <light9-color-picker color="{{value}}"></light9-color-picker>
         
       </div>
     </template>
-    <template is="dom-if" if="{{deviceAttr.useChoice}}">
-      <light9-listbox choices="{{deviceAttr.choices}}" value="{{value}}">
+    <template is="dom-if" if="{{deviceAttrRow.useChoice}}">
+      <light9-listbox choices="{{deviceAttrRow.choices}}" value="{{value}}">
       </light9-listbox>
     </template>
 
@@ -186,7 +185,13 @@
       <template is="dom-repeat" items="{{deviceAttrs}}" as="dattr">
         <div class="deviceAttr">
           <span>attr <resource-display minor graph="{{graph}}" uri="{{dattr.uri}}"></resource-display></span>
-          <light9-live-control device="{{uri}}" device-attr="{{dattr}}"></light9-live-control>
+          <light9-live-control
+            graph="{{graph}}"
+            device="{{uri}}"
+            device-attr-row="{{dattr}}"
+            effect="{{effect}}"
+            graph-to-controls="{{graphToControls}}"
+          ></light9-live-control>
         </div>
       </template>
     </div>
@@ -219,9 +224,7 @@
     </style>
     <rdfdb-synced-graph graph="{{graph}}"></rdfdb-synced-graph>
 
-    <light9-collector-client self="{{client}}"></light9-collector-client>
     <h1>device control
-      <button on-click="resendAll">resend all</button>
       <button on-click="clearAll">clear all</button></h1>
 
     <div id="save">
@@ -235,7 +238,12 @@
 
     <div id="deviceControls">
       <template is="dom-repeat" items="{{devices}}" as="device">
-        <light9-live-device-control graph="{{graph}}" uri="{{device.uri}}"></light9-live-device-control>
+        <light9-live-device-control
+          graph="{{graph}}"
+          uri="{{device.uri}}"
+          effect="{{effect}}"
+          graph-to-controls="{{graphToControls}}"
+        ></light9-live-device-control>
       </template>
     </div>
 
--- a/light9/web/live/live.coffee	Thu Jun 07 17:39:41 2018 +0000
+++ b/light9/web/live/live.coffee	Thu Jun 07 17:40:29 2018 +0000
@@ -1,32 +1,52 @@
 log = console.log
 
+valuePred = (graph, attr) ->
+  U = (x) -> graph.Uri(x)
+  scaledAttributeTypes = [U(':color'), U(':brightness'), U(':uv')]
+  if _.some(scaledAttributeTypes,
+            (x) -> attr.equals(x)) then U(':scaledValue') else U(':value')
+
 coffeeElementSetup(class Light9LiveControl extends Polymer.Element
   @is: 'light9-live-control'
   @getter_properties:
-    device: { type: String }
-    deviceAttr: { type: Object }
-    max: { type: Number, value: 1 }
+    graph: { type: Object, notify: true }
+    device: { type: Object }
+    deviceAttrRow: { type: Object } # object returned from attrRow, below
     value: { type: Object, notify: true }
     
     immediateSlider: { notify: true, observer: 'onSlider' }
     sliderWriteValue: { type: Number }
+
     pickedChoice: { observer: 'onChange' }
+    graphToControls: { type: Object }
   @getter_observers: [
     'onChange(value)'
+    'onGraphToControls(graphToControls)'
     ]
-  ready: ->
-    super.ready()
+  constructor: ->
+    super()
+    @enableChange = false # until 1st graph read
   onSlider: -> @value = @immediateSlider
   goBlack: -> @value = "#000000"
+  onGraphToControls: (gtc) ->
+    gtc.register(@device, @deviceAttrRow.uri, @graphValueChanged.bind(@))
+    @enableChange = true
+
+  graphValueChanged: (v) ->
+    log('change: control gets', v)
+    @enableChange = false
+    @value = v
+    @sliderWriteValue = v if @deviceAttrRow.useSlider
+    @enableChange = true
   onChange: (value) ->
-    @lastSent = [[@device, @deviceAttr.uri, value]]
-    @resend()
-  resend: ->
-    window.gather(@lastSent)
+    return unless @graphToControls? and @enableChange
+    log('change: control tells graph', @deviceAttrRow.uri.value, value)
+    @graphToControls.controlChanged(@device, @deviceAttrRow.uri, value)
+
   clear: ->
     @pickedChoice = null
     @sliderWriteValue = 0
-    if @deviceAttr.useColor
+    if @deviceAttrRow.useColor
       @value = '#000000'
     else
       @value = @immediateSlider = 0
@@ -37,8 +57,10 @@
   @getter_properties:
     graph: { type: Object, notify: true }
     uri: { type: String, notify: true }
+    effect: { type: String }
     deviceClass: { type: String, notify: true }
     deviceAttrs: { type: Array, notify: true }
+    graphToControls: { type: Object }
     bgStyle: { type: String, computed: '_bgStyle(deviceClass)' }
   @getter_observers: [
     'onGraph(graph)'
@@ -47,93 +69,203 @@
     hash = 0
     deviceClass = deviceClass.value
     for i in [(deviceClass.length-10)...deviceClass.length]
-        hash += deviceClass.charCodeAt(i)
+      hash += deviceClass.charCodeAt(i)
     hue = (hash * 8) % 360
     accent = "hsl(#{hue}, 49%, 22%)"
     "background: linear-gradient(to right, rgba(31,31,31,0) 50%, #{accent} 100%);"
+    
   onGraph: ->
     @graph.runHandler(@update.bind(@), "#{@uri.value} update")
-  update: ->
+    
+  update: (patch) ->
     U = (x) => @graph.Uri(x)
-    
+    return if patch? and not SyncedGraph.patchContainsPreds(
+      patch, [U('rdf:type'), U(':deviceAttr'), U(':dataType'), U(':choice')])
     @deviceClass = @graph.uriValue(@uri, U('rdf:type'))
-    
     @deviceAttrs = []
     for da in _.unique(@graph.sortedUris(@graph.objects(@deviceClass, U(':deviceAttr'))))
-      dataType = @graph.uriValue(da, U(':dataType'))
-      daRow = {
-        uri: da
-        dataType: dataType
-        showColorPicker: dataType.equals(U(':color'))
-        }
-      if dataType.equals(U(':color'))
-        daRow.useColor = true
+      @push('deviceAttrs', @attrRow(da))
 
-      else if dataType.equals(U(':choice'))
-        daRow.useChoice = true
-        choiceUris = @graph.sortedUris(@graph.objects(da, U(':choice')))
-        daRow.choices = ({uri: x, label: @graph.labelOrTail(x)} for x in choiceUris)
-        daRow.choiceSize = Math.min(choiceUris.length + 1, 10)
-      else
-        daRow.useSlider = true
+  attrRow: (devAttr) ->
+    U = (x) => @graph.Uri(x)
+    dataType = @graph.uriValue(devAttr, U(':dataType'))
+    daRow = {
+      uri: devAttr
+      dataType: dataType
+      showColorPicker: dataType.equals(U(':color'))
+      }
+    if dataType.equals(U(':color'))
+      daRow.useColor = true
+    else if dataType.equals(U(':choice'))
+      daRow.useChoice = true
+      choiceUris = @graph.sortedUris(@graph.objects(devAttr, U(':choice')))
+      daRow.choices = ({uri: x, label: @graph.labelOrTail(x)} for x in choiceUris)
+      daRow.choiceSize = Math.min(choiceUris.length + 1, 10)
+    else
+      daRow.useSlider = true
+      daRow.max = 1
+      if dataType.equals(U(':angle'))
+        # varies
         daRow.max = 1
-        if dataType.equals(U(':angle'))
-          # varies
-          daRow.max = 1
-
-      @push('deviceAttrs', daRow)
+    return daRow
+      
   clear: ->
     for lc in @shadowRoot.querySelectorAll("light9-live-control")
       lc.clear()
     
 )
+
+class GraphToControls
+  # More efficient bridge between liveControl widgets and graph edits,
+  # as opposed to letting each widget scan the graph and push lots of
+  # tiny patches to it.
+  constructor: (@graph) ->
+    @currentSettings = {} # {dev: {attr: value}}
+    @onChanged = {}
+    @effect = @graph.Uri('http://light9.bigasterisk.com/effect/pool_r')
+    @ctx = @graph.Uri('http://light9.bigasterisk.com/show/dance2017/effect/pool_r')
+    @graph.runHandler(@syncFromGraph.bind(@), 'GraphToControls sync')
+
+  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'))
+
+      pred = valuePred(@graph, devAttr)
+      try
+        value = @graph.floatValue(s, pred)
+      catch
+        value = @graph.stringValue(s, pred)
+      log('change: graph contains', devAttr, value)
+
+      oc = @onChanged[dev.value + " " + devAttr.value]
+      if oc?
+        log('change: gtc tells control')
+        oc(value)
+
+      # 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
+
+  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
+    return null
+
+    # faster one:
+    d = @currentSettings[device.value]
+    if d?
+      da = d[attr.value]
+      # ...
+    return null
+      
+  preview: ->
+    JSON.stringify(@currentSettings)
+
+  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
+
+  shouldBeStored: (deviceAttr, value) ->
+    # 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.
+    return value != 0 and value != '#000000'
+
+  controlChanged: (device, deviceAttr, value) ->
+    effectSetting = @effectSettingLookup(device, deviceAttr)
+    if @shouldBeStored(deviceAttr, value)
+      if not effectSetting?
+        @_addEffectSetting(device, deviceAttr, value)
+      else
+        @_patchExistingEffectSetting(effectSetting, deviceAttr, value)
+    else
+      @_removeEffectSetting(effectSetting)
+
+  _addEffectSetting: (device, deviceAttr, value) ->
+    U = (x) => @graph.Uri(x)
+    quad = (s, p, o) => @graph.Quad(s, p, o, @ctx)
+    effectSetting = @graph.nextNumberedResource(@effect.value + '_set')
+    addQuads = [
+      quad(@effect, U(':setting'), effectSetting),
+      quad(effectSetting, U(':device'),  device),
+      quad(effectSetting, U(':deviceAttr'), deviceAttr),
+      quad(effectSetting, valuePred(@graph, deviceAttr), @graph.prettyLiteral(value))
+    ]
+    patch = {addQuads: addQuads, delQuads: []}
+    log('save', patch)
+    @graph.applyAndSendPatch(patch)
+
+  _patchExistingEffectSetting: (effectSetting, deviceAttr, value) ->
+    log('patch existing', effectSetting.value)
+    @graph.patchObject(effectSetting, valuePred(@graph, deviceAttr), @graph.prettyLiteral(value), @ctx)
+
+  _removeEffectSetting: (effectSetting) ->
+    U = (x) => @graph.Uri(x)
+    if effectSetting?
+      toDel = [quad(@effect, U(':setting'), effectSetting, @ctx)]
+      for q in @graph.graph.getQuads(effectSetting)
+        toDel.push(q)
+      @graph.applyAndSendPatch({delQuads: toDel, addQuads: []})
+    
     
 coffeeElementSetup(class Light9LiveControls extends Polymer.Element
   @is: "light9-live-controls"
   @getter_properties:
     graph: { type: Object, notify: true }
-    client: { type: Object, notify: true }
     devices: { type: Array, notify: true }
-    currentSettings: { type: Object, notify: true } # dev+attr: [dev, attr, value]
     effectPreview: { type: String, notify: true }
     newEffectName: { type: String, notify: true }
     effect: { type: String, notify: true } # the one being edited, if any
+    graphToControls: { type: Object }
   @getter_observers: [
     'onGraph(graph)'
     'onEffect(effect)'
     ]
+
+  constructor: ->
+    super()
+    @graphToControls = null
   ready: ->
     super.ready()
     @currentSettings = {}
     @effectPreview = JSON.stringify({})
 
-    @sendAllThrottled = _.throttle(@sendAll.bind(@), 30)
-    
-    window.gather = (sent) =>
-      [dev, devAttr, value] = sent[0]
-      key = dev.value + " " + devAttr.value
-      # 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[key]
-      else
-        @currentSettings[key] = [dev, devAttr, value]
-      @effectPreview = JSON.stringify(v for k,v of @currentSettings)
+  onGraph: ->
+    @graphToControls = new GraphToControls(@graph)
+    @effect = @graphToControls.effect.value
+    log('set my @graphtocontrols')
+    @graph.runHandler(@update.bind(@), 'Light9LiveControls update')
 
-      @sendAllThrottled()
-
-  currentSettingsList: -> (v for k,v of @currentSettings)
+  effectSettingLookup: (device, attr) ->
+    if @graphToControls == null
+      throw new Error('not ready')
+    # optimization for getting the :setting node
+    return @graphToControls.effectSettingLookup(device, attr)
       
-  sendAll: ->
-    @client.send(@currentSettingsList())
-
-  valuePred: (attr) ->
-    U = (x) => @graph.Uri(x)
-    scaledAttributeTypes = [U(':color'), U(':brightness'), U(':uv')]
-    if [attr.equals(x) for x in scaledAttributeTypes].length then U(':scaledValue') else U(':value')
-
   onEffect: ->
     U = (x) => @graph.Uri(x)
     return unless @effect
@@ -142,7 +274,7 @@
       dev = @graph.uriValue(s, U(':device'))
       devAttr = @graph.uriValue(s, U(':deviceAttr'))
 
-      pred = @valuePred(devAttr)
+      pred = valuePred(@graph, devAttr)
       try
         value = @graph.floatValue(s, pred)
       catch
@@ -172,24 +304,14 @@
       addQuads.push(quad(@effect, U(':setting'), setting))
       addQuads.push(quad(setting, U(':device'), row[0]))
       addQuads.push(quad(setting, U(':deviceAttr'), row[1]))
-      value = if typeof(row[2]) == 'number'
-          @graph.LiteralRoundedFloat(row[2])
-        else
-          @graph.Literal(row[2])
-      addQuads.push(quad(setting, @valuePred(row[1]), value))
+   
+      addQuads.push(quad(setting, valuePred(@graph, row[1]), value))
       
     patch = {addQuads: addQuads, delQuads: []}
     log('save', patch)
     @graph.applyAndSendPatch(patch)
     @newEffectName = ''
-
-  onGraph: ->
-    @graph.runHandler(@update.bind(@), 'controls')
     
-  resendAll: ->
-    for llc in @getElementsByTagName("light9-live-control")
-      llc.resend()
-      
   clearAll: ->
     for dc in @shadowRoot.querySelectorAll("light9-live-device-control")
       dc.clear()
@@ -209,7 +331,7 @@
     # Tried columnize- fails in jquery maybe from weird elements.
     
     # not sure how to get this run after the children are created
-    setTimeout((() => $('#deviceControls').isotope({
+    setTimeout((() -> $('#deviceControls').isotope({
       # fitColumns would be nice, but it doesn't scroll vertically
       layoutMode: 'masonry',
       containerStyle: null