Files @ b62c78f35380
Branch filter:

Location: light9/light9/web/live/GraphToControls.ts

drewp@bigasterisk.com
bin/live vitejs runner

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