Files @ ad7ab7027907
Branch filter:

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

drewp@bigasterisk.com
clean up non-elements; get the lit elements at least to work with autoformat
import debug from "debug";
import { BlankNode, Literal, NamedNode, Quad_Object, Quad_Predicate, Quad_Subject, Term } from "n3";
import { some } from "underscore";
import { Patch } from "../patch";
import { SyncedGraph } from "../SyncedGraph";
import { ActiveSettings } from "./ActiveSettings";
const log = debug("g2c");

const valuePred = function (graph: SyncedGraph, attr: NamedNode) {
  const U = graph.U();
  const scaledAttributeTypes = [U(":color"), U(":brightness"), U(":uv")];
  if (some(scaledAttributeTypes, (x: NamedNode) => attr.equals(x))) {
    return U(":scaledValue");
  } else {
    return U(":value");
  }
};

function isUri(x: Term | number | string): x is NamedNode {
  return typeof x == "object" && x.termType == "NamedNode";
}

type ControlValue = number | string | NamedNode | null;
// Like element.set(path, newArray), but minimizes splices.
// Dotted paths don't work yet.
const syncArray = function (
  element: Element,
  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);
  }
};

export class GraphToControls {
  activeSettings: ActiveSettings;
  effect: NamedNode | null = null;
  ctx: NamedNode | null = null;
  // 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(public graph: SyncedGraph) {
    this.activeSettings = new ActiveSettings(this.graph);
  }

  ctxForEffect(effect: NamedNode): NamedNode {
    return this.graph.Uri(effect.value.replace("light9.bigasterisk.com/effect", "light9.bigasterisk.com/show/dance2019/effect"));
  }

  setEffect(effect: NamedNode) {
    this.clearSettings();
    this.effect = effect;
    this.ctx = this.ctxForEffect(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 = this.graph.U();
    const effect = this.graph.nextNumberedResource(U("http://light9.bigasterisk.com/effect/effect"));
    const ctx = this.ctxForEffect(effect);
    const quad = (s: Quad_Subject, p: Quad_Predicate, o: Quad_Object) => 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 = { adds: addQuads, dels: [] } as Patch;
    log("init new effect", patch);
    this.graph.applyAndSendPatch(patch);
    return effect;
  }

  syncFromGraph() {
    const U = this.graph.U();
    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")))) {
      if (!isUri(setting)) throw new Error();
      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: ControlValue) {
    // 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: NamedNode, deviceAttr: NamedNode, value: ControlValue) {
    // 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)

    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 {
      if (effectSetting !== null) {
        return this._removeEffectSetting(effectSetting);
      }
    }
  }

  _nodeForValue(value: ControlValue) {
    if (value === null) {
      throw new Error("no value");
    }
    if (isUri(value)) {
      return value;
    }
    return this.graph.prettyLiteral(value);
  }

  _addEffectSetting(device: NamedNode, deviceAttr: NamedNode, value: ControlValue) {
    log("change: _addEffectSetting", deviceAttr.value, value);
    const U = (x: string) => this.graph.Uri(x);
    const quad = (s: Quad_Subject, p: Quad_Predicate, o: Quad_Object) => this.graph.Quad(s, p, o, this.ctx);
    if (!this.effect) throw new Error("effect unset");
    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 = { adds: addQuads, dels: [] } as Patch;
    log("save", patch);
    return this.graph.applyAndSendPatch(patch);
  }

  _patchExistingEffectSetting(effectSetting: NamedNode, deviceAttr: NamedNode, value: ControlValue) {
    if (!this.ctx) throw new Error("no ctx");
    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: NamedNode) {
    const U = (x: string) => this.graph.Uri(x);
    if (effectSetting != null) {
      log("change: _removeEffectSetting", effectSetting.value);
      const toDel = [this.graph.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({ dels: toDel, adds: [] } as Patch);
      return this.activeSettings.deleteSetting(effectSetting);
    }
  }
}