Changeset - 499d5c6b153b
[Not reviewed]
default
0 2 0
Drew Perttula - 7 years ago 2018-06-07 23:04:50
drewp@bigasterisk.com
rewrite /live to edit effects in-graph
Ignore-this: ea45128f7bae6d17fda122991105eed
2 files changed with 146 insertions and 113 deletions:
0 comments (0 inline, 0 general)
light9/web/live/elements.html
Show inline comments
 
@@ -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">
 
@@ -247,7 +245,6 @@
 
      </template>
 
    </div>
 

	
 
    
 
  </template>
 
</dom-module>
 

	
light9/web/live/live.coffee
Show inline comments
 
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 @@ coffeeElementSetup(class Light9LiveContr
 
  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 @@ class GraphToControls
 
  # 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)
 

	
 
      # currentSettings is no longer what we need- the point is to
 
      # optimize effectSettingLookup by knowing the :setting nodes for
 
      # every dev+devAttr
 
      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)
 
                  
 
      # 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
 
    for key, row of toClear
 
      row.onChangeFunc(null)      
 
      @settings.delete(key)
 

	
 
  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
 
    return null
 
    key = device.value + " " + attr.value
 
    if @settings.has(key)
 
      return @settings.get(key).setting
 

	
 
    # 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 @@ class GraphToControls
 
    # 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 @@ class GraphToControls
 
    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 @@ class GraphToControls
 

	
 
  _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 @@ coffeeElementSetup(class Light9LiveContr
 
  @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)
 
  newEffect: ->
 
    @effectChoice = @graphToControls.newEffect().value
 

	
 
    @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))
 
      
 
    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
 

	
0 comments (0 inline, 0 general)