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

- effect name: - - + + +
-
@@ -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