diff --git a/light9/web/live/elements.html b/light9/web/live/elements.html
--- a/light9/web/live/elements.html
+++ b/light9/web/live/elements.html
@@ -224,16 +224,14 @@
-
device control
-
+ device control
@@ -246,7 +244,6 @@
>
-
diff --git a/light9/web/live/live.coffee b/light9/web/live/live.coffee
--- a/light9/web/live/live.coffee
+++ b/light9/web/live/live.coffee
@@ -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 @@ 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)
+ 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 @@ 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)
- @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