Mercurial > code > home > repos > light9
changeset 2081:c57cf4049004
dice up the live/ elements and code into ts files (no conversion yet except auto coffee->ts)
author | drewp@bigasterisk.com |
---|---|
date | Wed, 25 May 2022 00:06:00 -0700 |
parents | 6eb1fcbad5f6 |
children | b62c78f35380 |
files | light9/web/live/ActiveSettings.ts light9/web/live/GraphToControls.ts light9/web/live/Light9Listbox.ts light9/web/live/Light9LiveControl.ts light9/web/live/Light9LiveControls.ts light9/web/live/Light9LiveDeviceControl.ts light9/web/live/elements.html light9/web/live/index.html light9/web/live/live.coffee light9/web/live/vite.config.ts |
diffstat | 10 files changed, 936 insertions(+), 771 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/live/ActiveSettings.ts Wed May 25 00:06:00 2022 -0700 @@ -0,0 +1,98 @@ +class ActiveSettings { + graph: any; + settings: any; + keyForSetting: any; + onChanged: any; + constructor(graph: any) { + // 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} + this.graph = graph; + this.settings = new Map(); + this.keyForSetting = new Map(); // setting uri str -> dev+attr + + + + // Registered graphValueChanged funcs, by dev+attr. Kept even when + // settings are deleted. + this.onChanged = new Map(); + } + + addSettingsRow(device: { value: string; }, deviceAttr: { value: string; }, setting: { value: string; }, value: any) { + const key = device.value + " " + deviceAttr.value; + if (this.settings.has(key)) { + throw new Error("repeated setting on " + key); + } + if (this.keyForSetting.has(setting.value)) { + throw new Error("repeated keyForSetting on " + setting.value); + } + this.settings.set(key, { + setting, + onChangeFunc: this.onChanged[key], + jsValue: value + }); + this.keyForSetting.set(setting.value, key); + if (this.onChanged[key] != null) { + return this.onChanged[key](value); + } + } + + has(setting: { value: any; }) { + return this.keyForSetting.has(setting.value); + } + + setValue(setting: { value: any; }, value: any) { + const row = this.settings.get(this.keyForSetting.get(setting.value)); + row.jsValue = value; + if (row.onChangeFunc != null) { return row.onChangeFunc(value); } + } + + registerWidget(device: { value: string; }, deviceAttr: { value: string; }, graphValueChanged: any) { + const key = device.value + " " + deviceAttr.value; + this.onChanged[key] = graphValueChanged; + + if (this.settings.has(key)) { + const row = this.settings.get(key); + row.onChangeFunc = graphValueChanged; + return row.onChangeFunc(row.jsValue); + } + } + + effectSettingLookup(device: { value: string; }, attr: { value: string; }) { + const key = device.value + " " + attr.value; + if (this.settings.has(key)) { + return this.settings.get(key).setting; + } + + return null; + } + + deleteSetting(setting: { value: string; }) { + log('deleteSetting ' + setting.value); + const key = this.keyForSetting.get(setting.value); + const row = this.settings.get(key); + if ((row != null) && !row.setting.equals(setting)) { + throw new Error('corrupt row for ' + setting.value); + } + if ((row != null ? row.onChangeFunc : undefined) != null) { row.onChangeFunc(null); } + this.settings.delete(key); + return this.keyForSetting.delete(setting); + } + + clear() { + new Map(this.settings).forEach(function (row: { onChangeFunc: (arg0: any) => any; }, key: any) { + if (row.onChangeFunc != null) { return row.onChangeFunc(null); } + }); + this.settings.clear(); + return this.keyForSetting.clear(); + } + + forAll(cb: (arg0: any) => any) { + const all = Array.from(this.keyForSetting.keys()); + return Array.from(all).map((s: any) => cb(this.graph.Uri(s))); + } + + allSettingsStr() { + return this.keyForSetting.keys(); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/live/GraphToControls.ts Wed May 25 00:06:00 2022 -0700 @@ -0,0 +1,207 @@ + +const valuePred = function(graph: { Uri: (arg0: any) => any; }, attr: { equals: (arg0: any) => any; }) { + const U = (x: string) => graph.Uri(x); + const scaledAttributeTypes = [U(':color'), U(':brightness'), U(':uv')]; + if (_.some(scaledAttributeTypes, + ( x: any) => attr.equals(x))) { return U(':scaledValue'); } else { return U(':value'); } +}; + +const log = debug('live'); + +// Like element.set(path, newArray), but minimizes splices. +// Dotted paths don't work yet. +const syncArray = function(element: this, path: string, newArray: { length?: any; }, isElementEqual: { (a: any, b: any): boolean; (arg0: any, arg1: any): any; }) { + let pos = 0; + let 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) { + return element.splice('devices', pos, element[path].length - pos); + } +}; + + +class GraphToControls { + graph: any; + activeSettings: ActiveSettings; + effect: any; + ctx: any; + // 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: any) { + this.graph = graph; + this.activeSettings = new ActiveSettings(this.graph); + this.effect = null; + } + + ctxForEffect(effect: { value: { replace: (arg0: string, arg1: string) => any; }; }) { + return this.graph.Uri(effect.value.replace( + "light9.bigasterisk.com/effect", + "light9.bigasterisk.com/show/dance2019/effect")); + } + + setEffect(effect: any) { + this.clearSettings(); + this.effect = effect; + this.ctx = this.ctxForEffect(this.effect); + // are these going to pile up? consider @graph.triggerHandler('GTC sync') + return this.graph.runHandler(this.syncFromGraph.bind(this), 'GraphToControls sync'); + } + + newEffect() { + // wrong- this should be our editor's scratch effect, promoted to a + // real one when you name it. + const U = (x: string) => this.graph.Uri(x); + const effect = this.graph.nextNumberedResource(U('http://light9.bigasterisk.com/effect/effect')); + const ctx = this.ctxForEffect(effect); + const quad = (s: any, p: any, o: any) => this.graph.Quad(s, p, o, ctx); + + const addQuads = [ + quad(effect, U('rdf:type'), U(':Effect')), + quad(effect, U('rdfs:label'), this.graph.Literal(effect.value.replace(/.*\//, ""))), + quad(effect, U(':publishAttr'), U(':strength')) + ]; + const patch = { addQuads, delQuads: [] }; + log('init new effect', patch); + this.graph.applyAndSendPatch(patch); + return effect; + } + + syncFromGraph() { + const U = (x: string) => this.graph.Uri(x); + if (!this.effect) { return; } + log('syncFromGraph', this.effect); + + const toClear = new Set(this.activeSettings.allSettingsStr()); + + for (let setting of Array.from(this.graph.objects(this.effect, U(':setting')))) { + var value: { id: { match: (arg0: {}) => any; }; }; + const dev = this.graph.uriValue(setting, U(':device')); + const devAttr = this.graph.uriValue(setting, U(':deviceAttr')); + + const pred = valuePred(this.graph, devAttr); + try { + value = this.graph.uriValue(setting, pred); + if (!value.id.match(/^http/)) { + throw new Error("not uri"); + } + } catch (error) { + try { + value = this.graph.floatValue(setting, pred); + } catch (error1) { + value = this.graph.stringValue(setting, pred); + } + } + //log('change: graph contains', devAttr, value) + if (this.activeSettings.has(setting)) { + this.activeSettings.setValue(setting, value); + toClear.delete(setting.value); + } else { + this.activeSettings.addSettingsRow(dev, devAttr, setting, value); + } + } + + return Array.from(Array.from(toClear)).map((settingStr: any) => this.activeSettings.deleteSetting(U(settingStr))); + } + + clearSettings() { + return this.activeSettings.clear(); + } + + register(device: any, deviceAttr: any, graphValueChanged: any) { + return this.activeSettings.registerWidget(device, deviceAttr, graphValueChanged); + } + + shouldBeStored(deviceAttr: any, value: string | number) { + // 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 != null) && (value !== 0) && (value !== '#000000'); + } + + emptyEffect() { + return this.activeSettings.forAll(this._removeEffectSetting.bind(this)); + } + + controlChanged(device: any, deviceAttr: any, value: string) { + // todo: controls should be disabled if there's no effect and they won't do anything. + if (!this.effect) { return; } + + // value is float or #color or (Uri or null) + if ((value === undefined) || ((typeof value === "number") && isNaN(value)) || ((typeof value === "object") && (value !== null) && !value.id)) { + throw new Error("controlChanged sent bad value " + value); + } + const effectSetting = this.activeSettings.effectSettingLookup(device, deviceAttr); + + // sometimes this misses an existing setting, which leads to a mess + if (this.shouldBeStored(deviceAttr, value)) { + if ((effectSetting == null)) { + return this._addEffectSetting(device, deviceAttr, value); + } else { + return this._patchExistingEffectSetting(effectSetting, deviceAttr, value); + } + } else { + return this._removeEffectSetting(effectSetting); + } + } + + _nodeForValue(value: { id: any; }) { + if (value.id != null) { + return value; + } + return this.graph.prettyLiteral(value); + } + + _addEffectSetting(device: any, deviceAttr: { value: any; }, value: any) { + log('change: _addEffectSetting', deviceAttr.value, value); + const U = (x: string) => this.graph.Uri(x); + const quad = (s: any, p: any, o: any) => this.graph.Quad(s, p, o, this.ctx); + const effectSetting = this.graph.nextNumberedResource(this.effect.value + '_set'); + this.activeSettings.addSettingsRow(device, deviceAttr, effectSetting, value); + const addQuads = [ + quad(this.effect, U(':setting'), effectSetting), + quad(effectSetting, U(':device'), device), + quad(effectSetting, U(':deviceAttr'), deviceAttr), + quad(effectSetting, valuePred(this.graph, deviceAttr), this._nodeForValue(value)) + ]; + const patch = { addQuads, delQuads: [] }; + log('save', patch); + return this.graph.applyAndSendPatch(patch); + } + + _patchExistingEffectSetting(effectSetting: { value: any; }, deviceAttr: any, value: any) { + log('change: patch existing', effectSetting.value); + this.activeSettings.setValue(effectSetting, value); + return this.graph.patchObject(effectSetting, valuePred(this.graph, deviceAttr), this._nodeForValue(value), this.ctx); + } + + _removeEffectSetting(effectSetting: { value: any; }) { + const U = (x: string) => this.graph.Uri(x); + const quad = (s: any, p: any, o: any) => this.graph.Quad(s, p, o, this.ctx); + if (effectSetting != null) { + log('change: _removeEffectSetting', effectSetting.value); + const toDel = [quad(this.effect, U(':setting'), effectSetting, this.ctx)]; + for (let q of Array.from(this.graph.graph.getQuads(effectSetting))) { + toDel.push(q); + } + this.graph.applyAndSendPatch({ delQuads: toDel, addQuads: [] }); + return this.activeSettings.deleteSetting(effectSetting); + } + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/live/Light9Listbox.ts Wed May 25 00:06:00 2022 -0700 @@ -0,0 +1,72 @@ +<dom-module id="light9-listbox"> + <template> + <style> + paper-listbox { + --paper-listbox-background-color: none; + --paper-listbox-color: white; + --paper-listbox: { + /* measure biggest item? use flex for columns? */ + column-width: 9em; + } + } + paper-item { + --paper-item-min-height: 0; + --paper-item: { + display: block; + border: 1px outset #0f440f; + margin: 0 1px 5px 0; + background: #0b1d0b; + } + } + paper-item.iron-selected { + background: #7b7b4a; + } + </style> + <paper-listbox id="list" + selected="{{value}}" + attr-for-selected="uri" + on-focus-changed="selectOnFocus" + > + <paper-item on-focus="selectOnFocus">None</paper-item> + <template is="dom-repeat" items="{{choices}}"> + <paper-item on-focus="selectOnFocus" uri="{{item.uri}}">{{item.label}}</paper-item> + </template> + </paper-listbox> + + </template> + <script> + HTMLImports.whenReady(function () { + Polymer({ + is: "light9-listbox", + properties: { + choices: { type: Array }, + value: { type: String, notify: true }, + }, + observers: ['onValue(value)'], + selectOnFocus: function(ev) { + if (ev.target.uri === undefined) { + // *don't* clear for this, or we can't cycle through all choices (including none) with up/down keys + //this.clear(); + //return; + } + this.value = ev.target.uri; + + }, + onValue: function(value) { + if (value === null) { + this.clear(); + } + }, + clear: function() { + this.async(function() { + this.querySelectorAll('paper-item').forEach( + function(item) { item.blur(); }); + this.value = undefined; + }.bind(this)); + + }, + + }); + }); + </script> +</dom-module> \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/live/Light9LiveControl.ts Wed May 25 00:06:00 2022 -0700 @@ -0,0 +1,161 @@ + +<dom-module id="light9-live-control"> +<template> + <style> + #colorControls { + display: flex; + align-items: center; + } + #colorControls > * { + margin: 0 3px; + } + #colorControls paper-slider { + + } + paper-slider { width: 100%; height: 25px; } + </style> + + <style is="custom-style"> + paper-slider { + --paper-slider-knob-color: var(--paper-red-500); + --paper-slider-active-color: var(--paper-red-500); + + --paper-slider-font-color: white; + --paper-slider-input: { + width: 75px; + + background: black; + display: inline-block; + } + } + + </style> + + <template is="dom-if" if="{{deviceAttrRow.useSlider}}"> + <paper-slider min="0" + max="{{deviceAttrRow.max}}" + step=".001" + editable + content-type="application/json" + value="{{sliderWriteValue}}" + immediate-value="{{immediateSlider}}"></paper-slider> + </template> + <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="{{deviceAttrRow.useChoice}}"> + <light9-listbox choices="{{deviceAttrRow.choices}}" value="{{choiceValue}}"> + </light9-listbox> + </template> + +</template> + +</dom-module> + +const coffeeElementSetupLight9LiveControl = (function() { + class Light9LiveControl extends Polymer.Element { + static is: string; + static getter_properties: { + graph: { type: any; notify: boolean; }; device: { type: any; }; deviceAttrRow: { type: any; }; // object returned from attrRow, below + value: { type: any; notify: boolean; }; // null, Uri, float, str + choiceValue: { type: any; }; immediateSlider: { notify: boolean; observer: string; }; sliderWriteValue: { ...; }; pickedChoice: { ...; }; graphToControls: { ...; }; + }; + static getter_observers: {}; + enableChange: boolean; + value: any; + immediateSlider: any; + deviceAttrRow: any; + sliderWriteValue: { value: any; }; + choiceValue: any; + graphToControls: any; + graph: any; + pickedChoice: any; + static initClass() { + this.is = 'light9-live-control'; + this.getter_properties = { + graph: { type: Object, notify: true }, + device: { type: Object }, + deviceAttrRow: { type: Object }, // object returned from attrRow, below + value: { type: Object, notify: true }, // null, Uri, float, str + choiceValue: { type: Object }, + + immediateSlider: { notify: true, observer: 'onSlider' }, + sliderWriteValue: { type: Number }, + + pickedChoice: { observer: 'onChange' }, + graphToControls: { type: Object } + }; + this.getter_observers = [ + 'onChange(value)', + 'onGraphToControls(graphToControls)', + 'onChoice(choiceValue)' + ]; + } + constructor() { + super(); + this.enableChange = false; // until 1st graph read + } + onSlider() { return this.value = this.immediateSlider; } + goBlack() { return this.value = "#000000"; } + onGraphToControls(gtc: { register: (arg0: any, arg1: any, arg2: any) => void; }) { + gtc.register(this.device, this.deviceAttrRow.uri, this.graphValueChanged.bind(this)); + return this.enableChange = true; + } + device(device: any, uri: any, arg2: any) { + throw new Error("Method not implemented."); + } + + graphValueChanged(v: { value: any, }) { + log('change: control gets', v); + this.enableChange = false; + if (v === null) { + this.clear(); + } else { + this.value = v; + } + if (this.deviceAttrRow.useSlider) { this.sliderWriteValue = v; } + if (this.deviceAttrRow.useChoice) { this.choiceValue = (v === null ? v : v.value); } + return this.enableChange = true; + } + + onChoice(value: any) { + if ((this.graphToControls == null) || !this.enableChange) { return; } + if (value != null) { + value = this.graph.Uri(value); + } else { + value = null; + } + return this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, value); + } + + onChange(value: any) { + if ((this.graphToControls == null) || !this.enableChange) { return; } + if ((typeof value === "number") && isNaN(value)) { return; } // let onChoice do it + //log('change: control tells graph', @deviceAttrRow.uri.value, value) + if (value === undefined) { + value = null; + } + return this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, value); + } + + clear() { + this.pickedChoice = null; + this.sliderWriteValue = 0; + if (this.deviceAttrRow.useColor) { + return this.value = '#000000'; + } else if (this.deviceAttrRow.useChoice) { + return this.value = (this.pickedChoice = null); + } else { + return this.value = (this.immediateSlider = 0); + } + } + } + + Light9LiveControl.initClass(); + return Light9LiveControl; + })(); + \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/live/Light9LiveControls.ts Wed May 25 00:06:00 2022 -0700 @@ -0,0 +1,178 @@ + +<dom-module id="light9-live-controls"> +<template> + <style> + :host { + display: flex; + flex-direction: column; + } + #preview { + width: 100%; + } + #deviceControls { + flex-grow: 1; + position: relative; + width: 100%; + overflow-y: auto; + } + + light9-live-device-control > div { + break-inside: avoid-column; + } + light9-live-device-control { + + } + </style> + <rdfdb-synced-graph graph="{{graph}}"></rdfdb-synced-graph> + + <h1>device control</h1> + + <div id="save"> + <div> + <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> + </div> + + <div id="deviceControls"> + <template is="dom-repeat" items="{{devices}}" as="device"> + <light9-live-device-control + graph="{{graph}}" + uri="{{device.uri}}" + effect="{{effect}}" + graph-to-controls="{{graphToControls}}" + ></light9-live-device-control> + </template> + </div> + +</template> +</dom-module> + +const coffeeElementSetupLight9LiveControls = (function() { + class Light9LiveControls extends Polymer.Element { + static is: string; + static getter_properties: { + graph: { type: any; notify: boolean; }; devices: { type: any; notify: boolean; value: {}; }; + // string uri of the effect being edited, or null. This is the + // master value; GraphToControls follows. + effectChoice: { type: any; notify: boolean; value: any; }; graphToControls: { type: any; }; + }; + static getter_observers: {}; + graphToControls: any; + okToWriteUrl: boolean; + currentSettings: {}; + graph: any; + effectChoice: any; + static initClass() { + this.is = "light9-live-controls"; + this.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 } + }; + this.getter_observers = [ + 'onGraph(graph)', + 'onEffectChoice(effectChoice)' + ]; + } + + constructor() { + super(); + this.graphToControls = null; + this.okToWriteUrl = false; + } + + ready() { + super.ready(...arguments).ready(); + return this.currentSettings = {}; + } + + onGraph() { + this.graphToControls = new GraphToControls(this.graph); + this.graph.runHandler(this.update.bind(this), 'Light9LiveControls update'); + + // need graph to be loaded, so we don't make double settings? not sure. + return setTimeout(this.setFromUrl.bind(this), 1); + } + + setFromUrl() { + // not a continuous bidi link between url and effect; it only reads + // the url when the page loads. + const effect = new URL(window.location.href).searchParams.get('effect'); + if (effect != null) { + log('found url', effect); + this.effectChoice = effect; + } + return this.okToWriteUrl = true; + } + + writeToUrl(effectStr: any) { + if (!this.okToWriteUrl) { return; } + const u = new URL(window.location.href); + if (u.searchParams.get('effect') === effectStr) { + return; + } + u.searchParams.set('effect', effectStr); + window.history.replaceState({}, "", u.href); + return log('wrote new url', u.href); + } + + newEffect() { + return this.effectChoice = this.graphToControls.newEffect().value; + } + + onEffectChoice() { + const U = (x: any) => this.graph.Uri(x); + if ((this.effectChoice == null)) { + // unlink + if (this.graphToControls != null) { this.graphToControls.setEffect(null); } + } else { + log('load', this.effectChoice); + if (this.graphToControls != null) { this.graphToControls.setEffect(this.graph.Uri(this.effectChoice)); } + } + return this.writeToUrl(this.effectChoice); + } + + clearAll() { + // clears the effect! + return this.graphToControls.emptyEffect(); + } + + update() { + const U = (x: string) => this.graph.Uri(x); + + const newDevs = []; + for (let dc of Array.from(this.graph.sortedUris(this.graph.subjects(U('rdf:type'), U(':DeviceClass'))))) { + for (let dev of Array.from(this.graph.sortedUris(this.graph.subjects(U('rdf:type'), dc)))) { + if (this.graph.contains(dev, U(':hideInLiveUi'), null)) { + continue; + } + newDevs.push({uri: dev}); + } + } + + //log("controls update now has #{newDevs.length} devices") + syncArray(this, 'devices', newDevs, (a: { uri: { value: any; }; }, b: { uri: { value: any; }; }) => 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 + return setTimeout((() => $('#deviceControls').isotope({ + // fitColumns would be nice, but it doesn't scroll vertically + layoutMode: 'masonry', + containerStyle: null + })), 2000); + } + } + + Light9LiveControls.initClass(); + return Light9LiveControls; + })(); \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/live/Light9LiveDeviceControl.ts Wed May 25 00:06:00 2022 -0700 @@ -0,0 +1,200 @@ + +<dom-module id="light9-live-device-control"> +<template> + <style> + :host { + display: inline-block; + } + .device { + border: 2px solid #151e2d; + margin: 4px; + padding: 1px; + background: #171717; /* deviceClass gradient added later */ + break-inside: avoid-column; + width: 335px; + + } + .deviceAttr { + border-top: 1px solid #272727; + padding-bottom: 2px; + display: flex; + } + .deviceAttr > span { + + } + .deviceAttr > light9-live-control { + flex-grow: 1; + } + h2 { + font-size: 110%; + padding: 4px; + margin-top: 0; + margin-bottom: 0; + } + .device, h2 { + border-top-right-radius: 15px; + } + + #mainLabel { + font-size: 120%; + color: #9ab8fd; + text-decoration: initial; + } + .device.selected h2 { + outline: 3px solid #ffff0047; + } + .deviceAttr.selected { + background: #cada1829; + } + </style> + <div class$="device {{devClasses}}"> + <h2 style$="[[bgStyle]]" xon-click="onClick"> + <resource-display id="mainLabel" graph="{{graph}}" uri="{{uri}}"></resource-display> + a <resource-display minor graph="{{graph}}" uri="{{deviceClass}}"></resource-display> + </h2> + <template is="dom-repeat" items="{{deviceAttrs}}" as="dattr"> + <div xon-click="onAttrClick" class$="deviceAttr {{dattr.attrClasses}}"> + <span>attr <resource-display minor graph="{{graph}}" uri="{{dattr.uri}}"></resource-display></span> + <light9-live-control + graph="{{graph}}" + device="{{uri}}" + device-attr-row="{{dattr}}" + effect="{{effect}}" + graph-to-controls="{{graphToControls}}" + ></light9-live-control> + </div> + </template> + </div> +</template> +</dom-module> + +const coffeeElementSetupLight9LiveDeviceControl = (function() { + class Light9LiveDeviceControl extends Polymer.Element { + static is: string; + static getter_properties: { + graph: { type: any; notify: boolean; }; uri: { type: any; notify: boolean; }; effect: { type: any; }; deviceClass: { type: any; notify: boolean; }; // the uri str + deviceAttrs: { type: any; notify: boolean; }; graphToControls: { ...; }; bgStyle: { ...; }; devClasses: { ...; }; // the css kind + }; + static getter_observers: {}; + selectedAttrs: any; + graph: any; + uri: any; + devClasses: string; + deviceClass: any; + deviceAttrs: {}; + shadowRoot: any; + static initClass() { + this.is = "light9-live-device-control"; + this.getter_properties = { + graph: { type: Object, notify: true }, + uri: { type: String, notify: true }, + effect: { type: String }, + deviceClass: { type: String, notify: true }, // the uri str + deviceAttrs: { type: Array, notify: true }, + graphToControls: { type: Object }, + bgStyle: { type: String, computed: '_bgStyle(deviceClass)' }, + devClasses: { type: String, value: '' } // the css kind + }; + this.getter_observers = [ + 'onGraph(graph)' + ]; + } + constructor() { + super(); + this.selectedAttrs = new Set(); // uri strings + } + _bgStyle(deviceClass: { value: any, length: number, charCodeAt: (arg0: number) => number, }) { + let hash = 0; + deviceClass = deviceClass.value; + for (let start = deviceClass.length-10, i = start, end = deviceClass.length, asc = start <= end; asc ? i < end : i > end; asc ? i++ : i--) { + hash += deviceClass.charCodeAt(i); + } + const hue = (hash * 8) % 360; + const accent = `hsl(${hue}, 49%, 22%)`; + return `background: linear-gradient(to right, rgba(31,31,31,0) 50%, ${accent} 100%);`; + } + + onGraph() { + return this.graph.runHandler(this.update.bind(this), `${this.uri.value} update`); + } + + setDeviceSelected(isSel: any) { + return this.devClasses = isSel ? 'selected' : ''; + } + + setAttrSelected(devAttr: { value: any, }, isSel: any) { + if (isSel) { + this.selectedAttrs.add(devAttr.value); + } else { + this.selectedAttrs.delete(devAttr.value); + } + return this.update(); + } + + update(patch: null) { + const U = (x: string) => this.graph.Uri(x); + if ((patch != null) && !SyncedGraph.patchContainsPreds( + patch, [U('rdf:type'), U(':deviceAttr'), U(':dataType'), U(':choice')])) { return; } + this.deviceClass = this.graph.uriValue(this.uri, U('rdf:type')); + this.deviceAttrs = []; + return Array.from(_.unique(this.graph.sortedUris(this.graph.objects(this.deviceClass, U(':deviceAttr'))))).map((da: any) => + this.push('deviceAttrs', this.attrRow(da))); + } + push(arg0: string, arg1: { uri: { value: any, }, dataType: any, showColorPicker: any, attrClasses: string, }) { + throw new Error("Method not implemented."); + } + + attrRow(devAttr: { value: any, }) { + let x: { value: any; }; + const U = (x: string) => this.graph.Uri(x); + const dataType = this.graph.uriValue(devAttr, U(':dataType')); + const daRow = { + uri: devAttr, + dataType, + showColorPicker: dataType.equals(U(':color')), + attrClasses: this.selectedAttrs.has(devAttr.value) ? 'selected' : '' + }; + if (dataType.equals(U(':color'))) { + daRow.useColor = true; + } else if (dataType.equals(U(':choice'))) { + daRow.useChoice = true; + const choiceUris = this.graph.sortedUris(this.graph.objects(devAttr, U(':choice'))); + daRow.choices = ((() => { + const result = []; + for (x of Array.from(choiceUris)) { result.push({uri: x.value, label: this.graph.labelOrTail(x)}); + } + return result; + })()); + 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() { + return Array.from(this.shadowRoot.querySelectorAll("light9-live-control")).map((lc: { clear: () => any; }) => + lc.clear()); + } + + onClick(ev: any) { + return log('click', this.uri); + } + // select, etc + + onAttrClick(ev: { model: { dattr: { uri: any, }, }, }) { + return log('attr click', this.uri, ev.model.dattr.uri); + } + } + // select + + Light9LiveDeviceControl.initClass(); + return Light9LiveDeviceControl; + })(); + + \ No newline at end of file
--- a/light9/web/live/elements.html Tue May 24 23:32:19 2022 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,279 +0,0 @@ -<script src="/lib/debug/debug-build.js"></script> -<script> - debug.enable('*'); -</script> -<link rel="import" href="/lib/polymer/polymer.html"> - -<link rel="import" href="/lib/paper-slider/paper-slider.html"> -<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="../rdfdb-synced-graph.html"> -<link rel="import" href="../resource-display.html"> -<link rel="import" href="../light9-color-picker.html"> -<link rel="import" href="../edit-choice.html"> -<dom-module id="light9-listbox"> - <template> - <style> - paper-listbox { - --paper-listbox-background-color: none; - --paper-listbox-color: white; - --paper-listbox: { - /* measure biggest item? use flex for columns? */ - column-width: 9em; - } - } - paper-item { - --paper-item-min-height: 0; - --paper-item: { - display: block; - border: 1px outset #0f440f; - margin: 0 1px 5px 0; - background: #0b1d0b; - } - } - paper-item.iron-selected { - background: #7b7b4a; - } - </style> - <paper-listbox id="list" - selected="{{value}}" - attr-for-selected="uri" - on-focus-changed="selectOnFocus" - > - <paper-item on-focus="selectOnFocus">None</paper-item> - <template is="dom-repeat" items="{{choices}}"> - <paper-item on-focus="selectOnFocus" uri="{{item.uri}}">{{item.label}}</paper-item> - </template> - </paper-listbox> - - </template> - <script> - HTMLImports.whenReady(function () { - Polymer({ - is: "light9-listbox", - properties: { - choices: { type: Array }, - value: { type: String, notify: true }, - }, - observers: ['onValue(value)'], - selectOnFocus: function(ev) { - if (ev.target.uri === undefined) { - // *don't* clear for this, or we can't cycle through all choices (including none) with up/down keys - //this.clear(); - //return; - } - this.value = ev.target.uri; - - }, - onValue: function(value) { - if (value === null) { - this.clear(); - } - }, - clear: function() { - this.async(function() { - this.querySelectorAll('paper-item').forEach( - function(item) { item.blur(); }); - this.value = undefined; - }.bind(this)); - - }, - - }); - }); - </script> -</dom-module> - -<dom-module id="light9-live-control"> - <template> - <style> - #colorControls { - display: flex; - align-items: center; - } - #colorControls > * { - margin: 0 3px; - } - #colorControls paper-slider { - - } - paper-slider { width: 100%; height: 25px; } - </style> - - <style is="custom-style"> - paper-slider { - --paper-slider-knob-color: var(--paper-red-500); - --paper-slider-active-color: var(--paper-red-500); - - --paper-slider-font-color: white; - --paper-slider-input: { - width: 75px; - - background: black; - display: inline-block; - } - } - - </style> - - <template is="dom-if" if="{{deviceAttrRow.useSlider}}"> - <paper-slider min="0" - max="{{deviceAttrRow.max}}" - step=".001" - editable - content-type="application/json" - value="{{sliderWriteValue}}" - immediate-value="{{immediateSlider}}"></paper-slider> - </template> - <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="{{deviceAttrRow.useChoice}}"> - <light9-listbox choices="{{deviceAttrRow.choices}}" value="{{choiceValue}}"> - </light9-listbox> - </template> - - </template> - -</dom-module> - -<dom-module id="light9-live-device-control"> - <template> - <style> - :host { - display: inline-block; - } - .device { - border: 2px solid #151e2d; - margin: 4px; - padding: 1px; - background: #171717; /* deviceClass gradient added later */ - break-inside: avoid-column; - width: 335px; - - } - .deviceAttr { - border-top: 1px solid #272727; - padding-bottom: 2px; - display: flex; - } - .deviceAttr > span { - - } - .deviceAttr > light9-live-control { - flex-grow: 1; - } - h2 { - font-size: 110%; - padding: 4px; - margin-top: 0; - margin-bottom: 0; - } - .device, h2 { - border-top-right-radius: 15px; - } - - #mainLabel { - font-size: 120%; - color: #9ab8fd; - text-decoration: initial; - } - .device.selected h2 { - outline: 3px solid #ffff0047; - } - .deviceAttr.selected { - background: #cada1829; - } - </style> - <div class$="device {{devClasses}}"> - <h2 style$="[[bgStyle]]" xon-click="onClick"> - <resource-display id="mainLabel" graph="{{graph}}" uri="{{uri}}"></resource-display> - a <resource-display minor graph="{{graph}}" uri="{{deviceClass}}"></resource-display> - </h2> - <template is="dom-repeat" items="{{deviceAttrs}}" as="dattr"> - <div xon-click="onAttrClick" class$="deviceAttr {{dattr.attrClasses}}"> - <span>attr <resource-display minor graph="{{graph}}" uri="{{dattr.uri}}"></resource-display></span> - <light9-live-control - graph="{{graph}}" - device="{{uri}}" - device-attr-row="{{dattr}}" - effect="{{effect}}" - graph-to-controls="{{graphToControls}}" - ></light9-live-control> - </div> - </template> - </div> - </template> -</dom-module> - -<dom-module id="light9-live-controls"> - <template> - <style> - :host { - display: flex; - flex-direction: column; - } - #preview { - width: 100%; - } - #deviceControls { - flex-grow: 1; - position: relative; - width: 100%; - overflow-y: auto; - } - - light9-live-device-control > div { - break-inside: avoid-column; - } - light9-live-device-control { - - } - </style> - <rdfdb-synced-graph graph="{{graph}}"></rdfdb-synced-graph> - - <h1>device control</h1> - - <div id="save"> - <div> - <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> - </div> - - <div id="deviceControls"> - <template is="dom-repeat" items="{{devices}}" as="device"> - <light9-live-device-control - graph="{{graph}}" - uri="{{device.uri}}" - effect="{{effect}}" - graph-to-controls="{{graphToControls}}" - ></light9-live-device-control> - </template> - </div> - - </template> -</dom-module> - - -<script src="/node_modules/d3/dist/d3.min.js"></script> -<script src="/node_modules/n3/n3-browser.js"></script> -<script src="/lib/async/dist/async.js"></script> -<script src="/lib/underscore/underscore-min.js"></script> - -<script src="../coffee_element.js"></script> -<!-- see live.coffee - <script src="/lib/jquery/dist/jquery.js"></script> - <script src="/lib/isotope/dist/isotope.pkgd.min.js"></script> - <script src="/lib/isotope-fit-columns/fit-columns.js"></script> - <script src="/lib/jquery.columnizer/src/jquery.columnizer.js"></script> - --> - -<script src="live.js"></script>
--- a/light9/web/live/index.html Tue May 24 23:32:19 2022 -0700 +++ b/light9/web/live/index.html Wed May 25 00:06:00 2022 -0700 @@ -4,8 +4,6 @@ <title>device control</title> <meta charset="utf-8" /> <link rel="stylesheet" href="/style.css"> - <script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> - <link rel="import" href="elements.html"> </head> <body> <style>
--- a/light9/web/live/live.coffee Tue May 24 23:32:19 2022 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,490 +0,0 @@ -log = debug('live') - -# 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 } # null, Uri, float, str - choiceValue: { type: Object } - - immediateSlider: { notify: true, observer: 'onSlider' } - sliderWriteValue: { type: Number } - - pickedChoice: { observer: 'onChange' } - graphToControls: { type: Object } - @getter_observers: [ - 'onChange(value)' - 'onGraphToControls(graphToControls)' - 'onChoice(choiceValue)' - ] - 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 - @clear() - else - @value = v - @sliderWriteValue = v if @deviceAttrRow.useSlider - @choiceValue = (if v == null then v else v.value) if @deviceAttrRow.useChoice - @enableChange = true - - onChoice: (value) -> - return unless @graphToControls? and @enableChange - if value? - value = @graph.Uri(value) - else - value = null - @graphToControls.controlChanged(@device, @deviceAttrRow.uri, value) - - onChange: (value) -> - return unless @graphToControls? and @enableChange - return if typeof value == "number" and isNaN(value) # let onChoice do it - #log('change: control tells graph', @deviceAttrRow.uri.value, value) - if value == undefined - value = null - @graphToControls.controlChanged(@device, @deviceAttrRow.uri, value) - - clear: -> - @pickedChoice = null - @sliderWriteValue = 0 - if @deviceAttrRow.useColor - @value = '#000000' - else if @deviceAttrRow.useChoice - @value = @pickedChoice = null - 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 } # the uri str - deviceAttrs: { type: Array, notify: true } - graphToControls: { type: Object } - bgStyle: { type: String, computed: '_bgStyle(deviceClass)' } - devClasses: { type: String, value: '' } # the css kind - @getter_observers: [ - 'onGraph(graph)' - ] - constructor: -> - super() - @selectedAttrs = new Set() # uri strings - _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") - - setDeviceSelected: (isSel) -> - @devClasses = if isSel then 'selected' else '' - - setAttrSelected: (devAttr, isSel) -> - if isSel - @selectedAttrs.add(devAttr.value) - else - @selectedAttrs.delete(devAttr.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')) - attrClasses: if @selectedAttrs.has(devAttr.value) then 'selected' else '' - } - 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.value, 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() - - onClick: (ev) -> - log('click', @uri) - # select, etc - - onAttrClick: (ev) -> - log('attr click', @uri, ev.model.dattr.uri) - # select -) - - -class ActiveSettings - constructor: (@graph) -> - # 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() - @keyForSetting = new Map() # setting uri str -> dev+attr - - # Registered graphValueChanged funcs, by dev+attr. Kept even when - # settings are deleted. - @onChanged = new Map() - - addSettingsRow: (device, deviceAttr, setting, value) -> - key = device.value + " " + deviceAttr.value - if @settings.has(key) - throw new Error("repeated setting on "+key) - if @keyForSetting.has(setting.value) - throw new Error("repeated keyForSetting on "+setting.value) - @settings.set(key, { - setting: setting, - onChangeFunc: @onChanged[key], - jsValue: value - }) - @keyForSetting.set(setting.value, key) - if @onChanged[key]? - @onChanged[key](value) - - has: (setting) -> - @keyForSetting.has(setting.value) - - setValue: (setting, value) -> - row = @settings.get(@keyForSetting.get(setting.value)) - row.jsValue = value - row.onChangeFunc(value) if row.onChangeFunc? - - registerWidget: (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) - - effectSettingLookup: (device, attr) -> - key = device.value + " " + attr.value - if @settings.has(key) - return @settings.get(key).setting - - return null - - deleteSetting: (setting) -> - log('deleteSetting ' + setting.value) - key = @keyForSetting.get(setting.value) - row = @settings.get(key) - if row? and not row.setting.equals(setting) - throw new Error('corrupt row for ' + setting.value) - row.onChangeFunc(null) if row?.onChangeFunc? - @settings.delete(key) - @keyForSetting.delete(setting) - - clear: -> - new Map(@settings).forEach (row, key) -> - row.onChangeFunc(null) if row.onChangeFunc? - @settings.clear() - @keyForSetting.clear() - - forAll: (cb) -> - all = Array.from(@keyForSetting.keys()) - for s in all - cb(@graph.Uri(s)) - - allSettingsStr: -> - @keyForSetting.keys() - -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) -> - @activeSettings = new ActiveSettings(@graph) - @effect = null - - ctxForEffect: (effect) -> - @graph.Uri(effect.value.replace( - "light9.bigasterisk.com/effect", - "light9.bigasterisk.com/show/dance2019/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 - - syncFromGraph: -> - U = (x) => @graph.Uri(x) - return if not @effect - log('syncFromGraph', @effect) - - toClear = new Set(@activeSettings.allSettingsStr()) - - for setting in @graph.objects(@effect, U(':setting')) - dev = @graph.uriValue(setting, U(':device')) - devAttr = @graph.uriValue(setting, U(':deviceAttr')) - - pred = valuePred(@graph, devAttr) - try - value = @graph.uriValue(setting, pred) - if not value.id.match(/^http/) - throw new Error("not uri") - catch - try - value = @graph.floatValue(setting, pred) - catch - value = @graph.stringValue(setting, pred) - #log('change: graph contains', devAttr, value) - - if @activeSettings.has(setting) - @activeSettings.setValue(setting, value) - toClear.delete(setting.value) - else - @activeSettings.addSettingsRow(dev, devAttr, setting, value) - - for settingStr in Array.from(toClear) - @activeSettings.deleteSetting(U(settingStr)) - - clearSettings: -> - @activeSettings.clear() - - register: (device, deviceAttr, graphValueChanged) -> - @activeSettings.registerWidget(device, deviceAttr, graphValueChanged) - - 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? and value != 0 and value != '#000000' - - emptyEffect: -> - @activeSettings.forAll(@_removeEffectSetting.bind(@)) - - controlChanged: (device, deviceAttr, value) -> - # todo: controls should be disabled if there's no effect and they won't do anything. - return if not @effect - - # value is float or #color or (Uri or null) - if (value == undefined or (typeof value == "number" and isNaN(value)) or (typeof value == "object" and value != null and not value.id)) - throw new Error("controlChanged sent bad value " + value) - effectSetting = @activeSettings.effectSettingLookup(device, deviceAttr) - - # sometimes this misses an existing setting, which leads to a mess - - if @shouldBeStored(deviceAttr, value) - if not effectSetting? - @_addEffectSetting(device, deviceAttr, value) - else - @_patchExistingEffectSetting(effectSetting, deviceAttr, value) - else - @_removeEffectSetting(effectSetting) - - _nodeForValue: (value) -> - if value.id? - return value - return @graph.prettyLiteral(value) - - _addEffectSetting: (device, deviceAttr, value) -> - log('change: _addEffectSetting', deviceAttr.value, value) - U = (x) => @graph.Uri(x) - quad = (s, p, o) => @graph.Quad(s, p, o, @ctx) - effectSetting = @graph.nextNumberedResource(@effect.value + '_set') - @activeSettings.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), @_nodeForValue(value)) - ] - patch = {addQuads: addQuads, delQuads: []} - log('save', patch) - @graph.applyAndSendPatch(patch) - - _patchExistingEffectSetting: (effectSetting, deviceAttr, value) -> - log('change: patch existing', effectSetting.value) - @activeSettings.setValue(effectSetting, value) - @graph.patchObject(effectSetting, valuePred(@graph, deviceAttr), @_nodeForValue(value), @ctx) - - _removeEffectSetting: (effectSetting) -> - U = (x) => @graph.Uri(x) - quad = (s, p, o) => @graph.Quad(s, p, o, @ctx) - if effectSetting? - log('change: _removeEffectSetting', effectSetting.value) - toDel = [quad(@effect, U(':setting'), effectSetting, @ctx)] - for q in @graph.graph.getQuads(effectSetting) - toDel.push(q) - @graph.applyAndSendPatch({delQuads: toDel, addQuads: []}) - @activeSettings.deleteSetting(effectSetting) - - -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 - @okToWriteUrl = false - - ready: -> - super.ready() - @currentSettings = {} - - onGraph: -> - @graphToControls = new GraphToControls(@graph) - @graph.runHandler(@update.bind(@), 'Light9LiveControls update') - - # need graph to be loaded, so we don't make double settings? not sure. - setTimeout(@setFromUrl.bind(@), 1) - - setFromUrl: -> - # not a continuous bidi link between url and effect; it only reads - # the url when the page loads. - effect = new URL(window.location.href).searchParams.get('effect') - if effect? - log('found url', effect) - @effectChoice = effect - @okToWriteUrl = true - - writeToUrl: (effectStr) -> - return unless @okToWriteUrl - u = new URL(window.location.href) - if u.searchParams.get('effect') == effectStr - return - u.searchParams.set('effect', effectStr) - window.history.replaceState({}, "", u.href) - log('wrote new url', u.href) - - 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? - @writeToUrl(@effectChoice) - - 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)) - if @graph.contains(dev, U(':hideInLiveUi'), null) - continue - newDevs.push({uri: dev}) - - #log("controls update now has #{newDevs.length} devices") - 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) -) \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/light9/web/live/vite.config.ts Wed May 25 00:06:00 2022 -0700 @@ -0,0 +1,20 @@ +import { defineConfig } from "vite"; + +const servicePort = 8217; +export default defineConfig({ + base: "/live/", + root: "./light9/web/live", + publicDir: "../web", + server: { + host: "0.0.0.0", + strictPort: true, + port: servicePort + 100, + hmr: { + port: servicePort + 200, + }, + }, + clearScreen: false, + define: { + global: {}, + }, +});