Changeset - c57cf4049004
[Not reviewed]
default
2 1 7
drewp@bigasterisk.com - 3 years ago 2022-05-25 07:06:00
drewp@bigasterisk.com
dice up the live/ elements and code into ts files (no conversion yet except auto coffee->ts)
10 files changed with 936 insertions and 771 deletions:
0 comments (0 inline, 0 general)
light9/web/live/ActiveSettings.ts
Show inline comments
 
new file 100644
 
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();
 
  }
 
}
light9/web/live/GraphToControls.ts
Show inline comments
 
new file 100644
 

	
 
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);
 
    }
 
  }
 
}
light9/web/live/Light9Listbox.ts
Show inline comments
 
new file 100644
 
<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
light9/web/live/Light9LiveControl.ts
Show inline comments
 
new file 100644
 

	
 
<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
light9/web/live/Light9LiveControls.ts
Show inline comments
 
new file 100644
 

	
 
<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
light9/web/live/Light9LiveDeviceControl.ts
Show inline comments
 
new file 100644
 

	
 
<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
light9/web/live/elements.html
Show inline comments
 
deleted file
light9/web/live/index.html
Show inline comments
 
@@ -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>
light9/web/live/live.coffee
Show inline comments
 
deleted file
light9/web/live/vite.config.ts
Show inline comments
 
new file 100644
 
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: {},
 
  },
 
});
0 comments (0 inline, 0 general)