view light9/web/live/live.coffee @ 1802:3ffadb9f00cd

forgot a 2018 Ignore-this: b1dbaaf9ac317601ec0e9006907204a1
author drewp@bigasterisk.com
date Fri, 08 Jun 2018 05:52:52 +0000
parents 499d5c6b153b
children 7d68a9db8319
line wrap: on
line source

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')]
  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:
    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)'
    ]
  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
    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)
    @graphToControls.controlChanged(@device, @deviceAttrRow.uri, value)

  clear: ->
    @pickedChoice = null
    @sliderWriteValue = 0
    if @deviceAttrRow.useColor
      @value = '#000000'
    else
      @value = @immediateSlider = 0
)

coffeeElementSetup(class Light9LiveDeviceControl extends Polymer.Element
  @is: "light9-live-device-control"
  @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)'
    ]
  _bgStyle: (deviceClass) ->
    hash = 0
    deviceClass = deviceClass.value
    for i in [(deviceClass.length-10)...deviceClass.length]
      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: (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'))))
      @push('deviceAttrs', @attrRow(da))

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

    # Registered graphValueChanged funcs, by dev+attr
    @onChanged = {}

    # 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/dance2018/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)
    return if not @effect
    
    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(setting, pred)
      catch
        value = @graph.stringValue(setting, pred)
      log('change: graph contains', devAttr, 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)

  clearSettings: ->
    @settings.forEach (row, key) =>
      row.onChangeFunc(null) if row.onChangeFunc?

    @settings.clear()

  effectSettingLookup: (device, attr) ->
    key = device.value + " " + attr.value
    if @settings.has(key)
      return @settings.get(key).setting

    return null

  register: (device, deviceAttr, graphValueChanged) ->
    key = device.value + " " + deviceAttr.value

    @onChanged[key] = graphValueChanged

    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
    # 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'

  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?
        @_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')
    @addSettingsRow(device, deviceAttr, effectSetting, value)
    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)
    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)
        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 }
    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)'
    'onEffectChoice(effectChoice)'
    ]

  constructor: ->
    super()
    @graphToControls = null

  ready: ->
    super.ready()
    @currentSettings = {}

  onGraph: ->
    @graphToControls = new GraphToControls(@graph)
    @graph.runHandler(@update.bind(@), 'Light9LiveControls update')

  effectSettingLookup: (device, attr) ->
    if @graphToControls == null
      throw new Error('not ready')
    return @graphToControls.effectSettingLookup(device, attr)

  newEffect: ->
    @effectChoice = @graphToControls.newEffect().value
      
  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: ->
    # clears the effect!
    @graphToControls.emptyEffect()
    
  update: ->
    U = (x) => @graph.Uri(x)

    newDevs = []
    for dc in @graph.sortedUris(@graph.subjects(U('rdf:type'), U(':DeviceClass')))
      for dev in @graph.sortedUris(@graph.subjects(U('rdf:type'), dc))
        newDevs.push({uri: dev})

    syncArray(@, 'devices', newDevs, (a, b) -> a.uri.value == b.uri.value)

    return

    # Tried css columns- big slowdown from relayout as I'm scrolling.
    # Tried isotope- seems to only scroll to the right.
    # Tried columnize- fails in jquery maybe from weird elements.
    
    # not sure how to get this run after the children are created
    setTimeout((() -> $('#deviceControls').isotope({
      # fitColumns would be nice, but it doesn't scroll vertically
      layoutMode: 'masonry',
      containerStyle: null
      })), 2000)
)