Changeset - bbd6816d9e9e
[Not reviewed]
default
0 4 0
drewp@bigasterisk.com - 3 years ago 2022-05-29 09:54:57
drewp@bigasterisk.com
big optimization on effect editing. 50% time still in rebuildSettingsFromGraph though, and the rest is in setLabel
4 files changed with 18 insertions and 8 deletions:
0 comments (0 inline, 0 general)
light9/web/live/Effect.ts
Show inline comments
 
import debug from "debug";
 
import { Literal, NamedNode, Quad_Object, Quad_Predicate, Quad_Subject, Term } from "n3";
 
import { some } from "underscore";
 
import { Patch } from "../patch";
 
import { Patch, patchContainsPreds } from "../patch";
 
import { SyncedGraph } from "../SyncedGraph";
 

	
 
type Color = string;
 
export type ControlValue = number | Color | NamedNode;
 

	
 
const log = debug("effect");
 

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

	
 
function valuePred(graph: SyncedGraph, attr: NamedNode): 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");
 
  }
 
}
 

	
 
// effect settings data; r/w sync with the graph
 
export class Effect {
 
  private settings: Array<{ device: NamedNode; deviceAttr: NamedNode; setting: NamedNode; value: ControlValue }> = [];
 
@@ -35,90 +35,97 @@ export class Effect {
 
  ) {
 
    graph.runHandler(this.rebuildSettingsFromGraph.bind(this), `effect sync ${uri.value}`);
 
  }
 

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

	
 
  addNewEffectToGraph() {
 
    const U = this.graph.U();
 
    const ctx = this.ctxForEffect();
 
    const quad = (s: Quad_Subject, p: Quad_Predicate, o: Quad_Object) => this.graph.Quad(s, p, o, ctx);
 

	
 
    const addQuads = [
 
      quad(this.uri, U("rdf:type"), U(":Effect")),
 
      quad(this.uri, U("rdfs:label"), this.graph.Literal(this.uri.value.replace(/.*\//, ""))),
 
      quad(this.uri, U(":publishAttr"), U(":strength")),
 
    ];
 
    const patch = { adds: addQuads, dels: [] } as Patch;
 
    log("init new effect", patch);
 
    this.settings = [];
 
    this.graph.applyAndSendPatch(patch);
 
  }
 

	
 
  rebuildSettingsFromGraph() {
 
  rebuildSettingsFromGraph(patch?: Patch) {
 
    const U = this.graph.U();
 
    log("syncFromGraph", this.uri);
 
    if (patch && !patchContainsPreds(patch, [U(":setting"), U(":device"), U(":deviceAttr")])) {
 
      // that's an approx list of preds , but it just means we'll miss some pathological settings edits
 
    //   return;
 
    }
 

	
 
    // log("syncFromGraph", this.uri);
 

	
 
    const newSettings = [];
 

	
 
    for (let setting of Array.from(this.graph.objects(this.uri, U(":setting")))) {
 
      log(`  setting ${setting.value}`);
 
      //   log(`  setting ${setting.value}`);
 
      if (!isUri(setting)) throw new Error();
 
      let value: ControlValue;
 
      const device = this.graph.uriValue(setting, U(":device"));
 
      const deviceAttr = this.graph.uriValue(setting, U(":deviceAttr"));
 

	
 
      const pred = valuePred(this.graph, deviceAttr);
 
      try {
 
        value = this.graph.uriValue(setting, pred);
 
        if (!(value as NamedNode).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); // this may find multi values and throw
 
        }
 
      }
 
      log(`change: graph contains ${deviceAttr.value} ${value}`);
 
    //   log(`change: graph contains ${deviceAttr.value} ${value}`);
 

	
 
      newSettings.push({ device, deviceAttr, setting, value });
 
    }
 
    this.settings = newSettings;
 
    log(`rebuild to ${this.settings.length}`);
 
    this.onValuesChanged();
 
  }
 

	
 
  currentValue(device: NamedNode, deviceAttr: NamedNode): ControlValue | null {
 
    for (let s of this.settings) {
 
      if (device.equals(s.device) && deviceAttr.equals(s.deviceAttr)) {
 
        return s.value;
 
      }
 
    }
 
    return null;
 
  }
 

	
 
  // change this object now, but return the patch to be applied to the graph so it can be coalesced.
 
  edit(device: NamedNode, deviceAttr: NamedNode, newValue: ControlValue | null): Patch {
 
    log(`edit: value=${newValue}`);
 
    let existingSetting: NamedNode | null = null;
 
    for (let s of this.settings) {
 
      if (device.equals(s.device) && deviceAttr.equals(s.deviceAttr)) {
 
        if (existingSetting !== null) {
 
          throw new Error(`${this.uri.value} had two settings for ${device.value} - ${deviceAttr.value}`);
 
        }
 
        existingSetting = s.setting;
 
      }
 
    }
 

	
 
    if (newValue !== null && this.shouldBeStored(deviceAttr, newValue)) {
 
      if (existingSetting === null) {
 
        return this._addEffectSetting(device, deviceAttr, newValue);
 
      } else {
 
        return this._patchExistingEffectSetting(existingSetting, deviceAttr, newValue);
 
      }
 
    } else {
 
      if (existingSetting !== null) {
 
        return this._removeEffectSetting(existingSetting);
 
      }
 
    }
light9/web/live/GraphToControls.ts
Show inline comments
 
import debug from "debug";
 
import { NamedNode } from "n3";
 
import { SyncedGraph } from "../SyncedGraph";
 
import { ControlValue, Effect } from "./Effect";
 
const log = debug("g2c");
 

	
 
type NewValueCb = (newValue: ControlValue | null) => void;
 

	
 
// More efficient bridge between liveControl widgets and graph edits,
 
// More efficient bridge between liveControl widgets and graph edits (inside Effect),
 
// as opposed to letting each widget scan the graph and push lots of
 
// tiny patches to it.
 
export class GraphToControls {
 
  // rename to PageControls?
 
  effect: Effect | null = null; // this uri should sync to the editchoice
 
  registeredWidgets: Map<NamedNode, Map<NamedNode, NewValueCb>> = new Map();
 
  constructor(public graph: SyncedGraph) {}
 

	
 
  setEffect(effect: NamedNode | null) {
 
    log(`setEffect ${effect?.value}`);
 
    this.effect = effect ? new Effect(this.graph, effect, this.onValuesChanged.bind(this)) : null;
 
  }
 

	
 
  newEffect(): NamedNode {
 
    // wrong- this should be our editor's scratch effect, promoted to a
 
    // real one when you name it.
 
    const uri = this.graph.nextNumberedResource(this.graph.Uri("http://light9.bigasterisk.com/effect/effect"));
 

	
 
    this.effect = new Effect(this.graph, uri, this.onValuesChanged.bind(this));
 
    log("add new eff");
 
    this.effect.addNewEffectToGraph();
 
    return this.effect.uri;
 
  }
 

	
light9/web/live/Light9DeviceControl.ts
Show inline comments
 
@@ -125,49 +125,49 @@ export class Light9DeviceControl extends
 
    const u = deviceClass.value;
 
    for (let i = u.length - 10; i < u.length; i++) {
 
      hash += u.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%);`;
 
  }
 

	
 
  setDeviceSelected(isSel: any) {
 
    this.devClasses = isSel ? "selected" : "";
 
  }
 

	
 
  setAttrSelected(devAttr: NamedNode, isSel: boolean) {
 
    if (isSel) {
 
      this.selectedAttrs.add(devAttr);
 
    } else {
 
      this.selectedAttrs.delete(devAttr);
 
    }
 
    // this.syncDeviceAttrsFromGraph();
 
  }
 

	
 
  syncDeviceAttrsFromGraph(patch?: Patch) {
 
    const U = this.graph.U();
 
    if (patch != null && !patchContainsPreds(patch, [U("rdf:type"), U(":deviceAttr"), U(":dataType"), U(":choice")])) {
 
    if (patch && !patchContainsPreds(patch, [U("rdf:type"), U(":deviceAttr"), U(":dataType"), U(":choice")])) {
 
      return;
 
    }
 
    try {
 
    this.deviceClass = this.graph.uriValue(this.uri, U("rdf:type"));
 
    } catch(e) {
 
      // what's likely is we're going through a graph reload and the graph 
 
      // is gone but the controls remain
 
    }
 
    this.deviceAttrs = [];
 
    Array.from(unique(this.graph.sortedUris(this.graph.objects(this.deviceClass, U(":deviceAttr"))))).map((da: NamedNode) =>
 
      this.deviceAttrs.push(this.attrRow(da))
 
    );
 
    this.requestUpdate();
 
  }
 

	
 
  attrRow(devAttr: NamedNode): DeviceAttrRow {
 
    let x: NamedNode;
 
    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) ? "selected" : "",
light9/web/live/Light9LiveControls.ts
Show inline comments
 
import debug from "debug";
 
import { css, html, LitElement, PropertyValues } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 
import { NamedNode } from "n3";
 
import { sortBy, uniq } from "underscore";
 
import { Patch } from "../patch";
 
import { Patch, patchContainsPreds } from "../patch";
 
import { getTopGraph } from "../RdfdbSyncedGraph";
 
import { SyncedGraph } from "../SyncedGraph";
 
import { GraphToControls } from "./GraphToControls";
 
export { Light9DeviceControl as Light9LiveDeviceControl } from "./Light9DeviceControl";
 
export { EditChoice } from "../EditChoice";
 
const log = debug("controls");
 

	
 
@customElement("light9-live-controls")
 
export class Light9LiveControls extends LitElement {
 
  graph!: SyncedGraph;
 

	
 
  static styles = [
 
    css`
 
      :host {
 
        display: flex;
 
        flex-direction: column;
 
      }
 
      #preview {
 
        width: 100%;
 
      }
 
      #deviceControls {
 
        flex-grow: 1;
 
        position: relative;
 
        width: 100%;
 
@@ -69,48 +69,51 @@ export class Light9LiveControls extends 
 
  graphToControls!: GraphToControls;
 
  okToWriteUrl: boolean = false;
 

	
 
  constructor() {
 
    super();
 

	
 
    getTopGraph().then((g) => {
 
      this.graph = g;
 
      this.graph.runHandler(this.findDevices.bind(this), "findDevices");
 
      this.graphToControls = new GraphToControls(this.graph);
 
      // this.graph.runHandler(this.xupdate.bind(this), "Light9LiveControls update");
 
      this.setEffectFromUrl();
 
    });
 
  }
 

	
 
  updated(changedProperties: PropertyValues) {
 
    if (changedProperties.has("effectChoice")) {
 
      log(`effectChoice to ${this.effectChoice?.value}`);
 
      this.onEffectChoice();
 
    }
 
  }
 

	
 
  findDevices(patch?: Patch) {
 
    const U = this.graph.U();
 
    if (patch && !patchContainsPreds(patch, [U("rdf:type")])) {
 
      return;
 
    }
 

	
 
    this.devices = [];
 
    let classes = this.graph.subjects(U("rdf:type"), U(":DeviceClass"));
 
    log(`found ${classes.length} device classes`)
 
    uniq(sortBy(classes, "value"), true).forEach((dc) => {
 
      sortBy(this.graph.subjects(U("rdf:type"), dc), "value").forEach((dev) => {
 
        this.devices.push(dev as NamedNode);
 
      });
 
    });
 
    this.requestUpdate();
 
  }
 

	
 
  setEffectFromUrl() {
 
    // 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 effect in url ${effect}`);
 
      this.effectChoice = this.graph.Uri(effect);
 
    }
 
    this.okToWriteUrl = true;
 
  }
 

	
 
  writeToUrl(effect: NamedNode | null) {
0 comments (0 inline, 0 general)