Changeset - 1b6e7016e3de
[Not reviewed]
default
1 6 1
drewp@bigasterisk.com - 3 years ago 2022-05-29 08:49:34
drewp@bigasterisk.com
rewrite state mgmt in live/
8 files changed with 411 insertions and 460 deletions:
0 comments (0 inline, 0 general)
light9/web/RdfdbSyncedGraph.ts
Show inline comments
 
import debug from "debug";
 
import { html, LitElement, css } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 
import { NamedNode } from "n3";
 
import { Patch } from "./patch";
 
import { SyncedGraph } from "./SyncedGraph";
 

	
 
const log = debug("syncedgraph-el");
 

	
 

	
 
// todo: consider if this has anything to contribute:
 
// https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md
 
let setTopGraph: (sg: SyncedGraph) => void;
 
(window as any).topSyncedGraph = new Promise<SyncedGraph>((res, rej) => {
 
  setTopGraph = res;
 
});
 

	
 
// Contains a SyncedGraph,
 
@@ -39,29 +41,29 @@ export class RdfdbSyncedGraph extends Li
 
    console.log("reset");
 
  }
 

	
 
  constructor() {
 
    super();
 
    this.status = "startup";
 
    const prefixes = new Map<string, string>([
 
      ["", "http://light9.bigasterisk.com/"],
 
      ["dev", "http://light9.bigasterisk.com/device/"],
 
      ["rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"],
 
      ["rdfs", "http://www.w3.org/2000/01/rdf-schema#"],
 
      ["xsd", "http://www.w3.org/2001/XMLSchema#"],
 
    ]);
 
    this.graph = new SyncedGraph(
 
      this.testGraph ? null : "/rdfdb/api/syncedGraph",
 
      {
 
        "": "http://light9.bigasterisk.com/",
 
        dev: "http://light9.bigasterisk.com/device/",
 
        rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
 
        rdfs: "http://www.w3.org/2000/01/rdf-schema#",
 
        xsd: "http://www.w3.org/2001/XMLSchema#",
 
      },
 
      prefixes,
 
      (s: string) => {
 
        this.status = s;
 
      },
 
      this.onClear.bind(this),
 
      this.onClear.bind(this)
 
    );
 
    setTopGraph(this.graph);
 
  }
 

	
 
}
 

	
 
export async function getTopGraph(): Promise<SyncedGraph> {
 
  const s = (window as any).topSyncedGraph;
 
  return await s;
 
}
light9/web/SyncedGraph.ts
Show inline comments
 
import * as d3 from "d3";
 
import debug from "debug";
 
import * as N3 from "n3";
 
import { Quad, Quad_Object, Quad_Predicate, Quad_Subject } from "n3";
 
import { sortBy, unique } from "underscore";
 
import { AutoDependencies, HandlerFunc } from "./AutoDependencies";
 
import { Patch, patchSizeSummary } from "./patch";
 
@@ -13,31 +12,32 @@ const RDF = "http://www.w3.org/1999/02/2
 
export class SyncedGraph {
 
  private _autoDeps: AutoDependencies;
 
  private _client: RdfDbClient;
 
  private graph: N3.Store;
 
  cachedFloatValues: any;
 
  cachedUriValues: any;
 
  prefixFuncs: (x: string) => string = (x) => x;
 
  private prefixFuncs: (prefix: string) => N3.PrefixedToIri;
 
  serial: any;
 
  _nextNumber: any;
 
  // Main graph object for a browser to use. Consider using RdfdbSyncedGraph element to create & own
 
  // one of these. Syncs both ways with rdfdb. Meant to hide the choice of RDF lib, so we can change it
 
  // later.
 
  //
 
  // Note that _applyPatch is the only method to write to the graph, so
 
  // it can fire subscriptions.
 

	
 
  constructor(
 
    // url is the /syncedGraph path of an rdfdb server.
 
    public url: any,
 
    // prefixes can be used in Uri(curie) calls.
 
    public prefixes: { [short: string]: string },
 
    // prefixes can be used in Uri(curie) calls. This mapping may grow during loadTrig calls.
 
    public prefixes: Map<string, string>,
 
    private setStatus: any,
 
    // called if we clear the graph
 
    private clearCb: any
 
  ) {
 
    this.prefixFuncs = this.rebuildPrefixFuncs(prefixes);
 
    this.graph = new N3.Store();
 
    this._autoDeps = new AutoDependencies();
 
    this.clearGraph();
 

	
 
    this._client = new RdfDbClient(this.url, this._clearGraphOnNewConnection.bind(this), this._applyPatch.bind(this), this.setStatus);
 
  }
 
@@ -47,33 +47,35 @@ export class SyncedGraph {
 
    this.cachedFloatValues = new Map(); // s + '|' + p -> number
 
    this.cachedUriValues = new Map(); // s + '|' + p -> Uri
 

	
 
    this._applyPatch({ adds: [], dels: this.graph.getQuads(null, null, null, null) });
 
    // if we had a Store already, this lets N3.Store free all its indices/etc
 
    this.graph = new N3.Store();
 
    this._addPrefixes(this.prefixes);
 
    this.rebuildPrefixFuncs(this.prefixes);
 
  }
 

	
 
  _clearGraphOnNewConnection() {
 
    // must not send a patch to the server!
 
    log("clearGraphOnNewConnection");
 
    this.clearGraph();
 
    log("clearGraphOnNewConnection done");
 
    if (this.clearCb != null) {
 
      return this.clearCb();
 
    }
 
  }
 

	
 
  _addPrefixes(prefixes: { [x: string]: string }) {
 
    for (let k of Array.from(prefixes || {})) {
 
      this.prefixes[k] = prefixes[k];
 
    }
 
    this.prefixFuncs = N3.Util.prefixes(this.prefixes);
 
  private rebuildPrefixFuncs(prefixes: Map<string, string>) {
 
    const p = Object.create(null);
 
    prefixes.forEach((v: string, k: string) => (p[k] = v));
 

	
 
    this.prefixFuncs = N3.Util.prefixes(p);
 
    return this.prefixFuncs;
 
  }
 

	
 
  U() { // just a shorthand
 
  U() {
 
    // just a shorthand
 
    return this.Uri.bind(this);
 
  }
 

	
 
  Uri(curie: string) {
 
    if (curie == null) {
 
      throw new Error("no uri");
 
@@ -82,18 +84,27 @@ export class SyncedGraph {
 
      return N3.DataFactory.namedNode(curie);
 
    }
 
    const part = curie.split(":");
 
    return this.prefixFuncs(part[0])(part[1]);
 
  }
 

	
 
  Literal(jsValue: any) {
 
  // Uri(shorten(u)).value==u
 
  shorten(uri: N3.NamedNode): string {
 
    const prefix = "http://light9.bigasterisk.com/";
 
    if (uri.value.startsWith(prefix)) {
 
      return ":" + uri.value.substring(prefix.length);
 
    }
 
    return uri.value;
 
  }
 

	
 
  Literal(jsValue: string | number) {
 
    return N3.DataFactory.literal(jsValue);
 
  }
 

	
 
  LiteralRoundedFloat(f: number) {
 
    return N3.DataFactory.literal(d3.format(".3f")(f), this.Uri("http://www.w3.org/2001/XMLSchema#double"));
 
    return N3.DataFactory.literal(f.toPrecision(3), this.Uri("http://www.w3.org/2001/XMLSchema#double"));
 
  }
 

	
 
  Quad(s: any, p: any, o: any, g: any) {
 
    return N3.DataFactory.quad(s, p, o, g);
 
  }
 

	
 
@@ -111,13 +122,13 @@ export class SyncedGraph {
 
        throw new Error(error);
 
      }
 
      if (quad) {
 
        return patch.adds.push(quad);
 
      } else {
 
        this._applyPatch(patch);
 
        this._addPrefixes(prefixes);
 
        // todo: here, add those prefixes to our known set
 
        if (cb) {
 
          return cb();
 
        }
 
      }
 
    });
 
  }
 
@@ -142,13 +153,13 @@ export class SyncedGraph {
 
    this._validatePatch(patch);
 

	
 
    this._applyPatch(patch);
 
    if (this._client) {
 
      this._client.sendPatch(patch);
 
    }
 
    return console.timeEnd("applyAndSendPatch");
 
    console.timeEnd("applyAndSendPatch");
 
  }
 

	
 
  _validatePatch(patch: Patch) {
 
    return [patch.adds, patch.dels].map((qs: Quad[]) =>
 
      (() => {
 
        const result = [];
 
@@ -168,13 +179,13 @@ export class SyncedGraph {
 
  }
 

	
 
  _applyPatch(patch: Patch) {
 
    // In most cases you want applyAndSendPatch.
 
    //
 
    // This is the only method that writes to this.graph!
 
    log("patch from server [1]")
 
    log("patch from server [1]");
 
    this.cachedFloatValues.clear();
 
    this.cachedUriValues.clear();
 
    for (let quad of Array.from(patch.dels)) {
 
      //log("remove #{JSON.stringify(quad)}")
 
      const did = this.graph.removeQuad(quad);
 
    }
 
@@ -296,12 +307,18 @@ export class SyncedGraph {
 
  subjects(p: any, o: any): Quad_Subject[] {
 
    this._autoDeps.askedFor(null, p, o, null);
 
    const quads = this.graph.getQuads(null, p, o, null);
 
    return Array.from(quads).map((q: { subject: any }) => q.subject);
 
  }
 

	
 
  subjectStatements(s: Quad_Subject): Quad[] {
 
    this._autoDeps.askedFor(s, null, null, null);
 
    const quads = this.graph.getQuads(s, null, null, null);
 
    return quads;
 
  }
 

	
 
  items(list: any) {
 
    const out = [];
 
    let current = list;
 
    while (true) {
 
      if (current === RDF + "nil") {
 
        break;
light9/web/live/ActiveSettings.ts
Show inline comments
 
deleted file
light9/web/live/Effect.ts
Show inline comments
 
new file 100644
 
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 { 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 }> = [];
 

	
 
  constructor(
 
    public graph: SyncedGraph,
 
    public uri: NamedNode,
 
    // called if the graph changes our values and not when the caller uses edit()
 
    private onValuesChanged: (values: void) => void
 
  ) {
 
    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() {
 
    const U = this.graph.U();
 
    log("syncFromGraph", this.uri);
 

	
 
    const newSettings = [];
 

	
 
    for (let setting of Array.from(this.graph.objects(this.uri, U(":setting")))) {
 
      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}`);
 

	
 
      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);
 
      }
 
    }
 
    return { adds: [], dels: [] };
 
  }
 

	
 
  shouldBeStored(deviceAttr: NamedNode, value: ControlValue | null): boolean {
 
    // 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";
 
  }
 

	
 
  _addEffectSetting(device: NamedNode, deviceAttr: NamedNode, value: ControlValue): Patch {
 
    log("  _addEffectSetting", deviceAttr.value, value);
 
    const U = (x: string) => this.graph.Uri(x);
 
    const ctx = this.ctxForEffect();
 
    const quad = (s: Quad_Subject, p: Quad_Predicate, o: Quad_Object) => this.graph.Quad(s, p, o, ctx);
 
    if (!this.uri) throw new Error("effect unset");
 
    const setting = this.graph.nextNumberedResource(this.uri.value + "_set");
 

	
 
    const addQuads = [
 
      quad(this.uri, U(":setting"), setting),
 
      quad(setting, U(":device"), device),
 
      quad(setting, U(":deviceAttr"), deviceAttr),
 
      quad(setting, valuePred(this.graph, deviceAttr), this._nodeForValue(value)),
 
    ];
 
    const patch = { adds: addQuads, dels: [] } as Patch;
 
    log("  save", patch);
 
    this.settings.push({ device, deviceAttr, setting, value });
 
    return patch;
 
  }
 

	
 
  _patchExistingEffectSetting(effectSetting: NamedNode, deviceAttr: NamedNode, value: ControlValue): Patch {
 
    log("  patch existing", effectSetting.value);
 
    return this.graph.getObjectPatch(
 
      effectSetting, //
 
      valuePred(this.graph, deviceAttr),
 
      this._nodeForValue(value),
 
      this.ctxForEffect()
 
    );
 
  }
 

	
 
  _removeEffectSetting(effectSetting: NamedNode): Patch {
 
    const U = (x: string) => this.graph.Uri(x);
 
    log("  _removeEffectSetting", effectSetting.value);
 
    const toDel = [this.graph.Quad(this.uri, U(":setting"), effectSetting, this.ctxForEffect())];
 
    for (let q of this.graph.subjectStatements(effectSetting)) {
 
      toDel.push(q);
 
    }
 
    return { dels: toDel, adds: [] };
 
  }
 

	
 
  _nodeForValue(value: ControlValue): NamedNode | Literal {
 
    if (value === null) {
 
      throw new Error("no value");
 
    }
 
    if (isUri(value)) {
 
      return value;
 
    }
 
    return this.graph.prettyLiteral(value);
 
  }
 
}
light9/web/live/GraphToControls.ts
Show inline comments
 
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 { NamedNode } from "n3";
 
import { SyncedGraph } from "../SyncedGraph";
 
import { ActiveSettings } from "./ActiveSettings";
 
import { ControlValue, Effect } from "./Effect";
 
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 NewValueCb = (newValue: ControlValue | null) => void;
 

	
 
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"));
 
  }
 
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) {
 
    this.clearSettings();
 
    this.effect = effect;
 
    this.ctx = !effect ? null : this.ctxForEffect(effect);
 
    // are these going to pile up? consider @graph.triggerHandler('GTC sync')
 
    this.graph.runHandler(this.syncFromGraph.bind(this), "GraphToControls sync");
 
    log(`setEffect ${effect?.value}`);
 
    this.effect = effect ? new Effect(this.graph, effect, this.onValuesChanged.bind(this)) : null;
 
  }
 

	
 
  newEffect() {
 
  newEffect(): NamedNode {
 
    // 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 uri = this.graph.nextNumberedResource(this.graph.Uri("http://light9.bigasterisk.com/effect/effect"));
 

	
 
    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;
 
    this.effect = new Effect(this.graph, uri, this.onValuesChanged.bind(this));
 
    log("add new eff");
 
    this.effect.addNewEffectToGraph();
 
    return this.effect.uri;
 
  }
 

	
 
  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);
 
      }
 
  onValuesChanged() {
 
    log(`i learned values changed for ${this.effect?.uri.value} `);
 
    this.registeredWidgets.forEach((d1: Map<NamedNode, NewValueCb>, device: NamedNode) => {
 
      d1.forEach((cb: NewValueCb, deviceAttr: NamedNode) => {
 
        const v = this.effect ? this.effect.currentValue(device, deviceAttr) : null;
 
        cb(v);
 
      });
 
    });
 
    }
 

	
 
    return Array.from(Array.from(toClear)).map((settingStr: any) => this.activeSettings.deleteSetting(U(settingStr)));
 
  register(device: NamedNode, deviceAttr: NamedNode, graphValueChanged: NewValueCb) {
 
    // log(`control for ${device.value}-${deviceAttr.value} registring with g2c`);
 
    let d1 = this.registeredWidgets.get(device);
 
    if (!d1) {
 
      d1 = new Map();
 
      this.registeredWidgets.set(device, d1);
 
  }
 

	
 
  clearSettings() {
 
    return this.activeSettings.clear();
 
  }
 

	
 
  register(device: any, deviceAttr: any, graphValueChanged: any) {
 
    return this.activeSettings.registerWidget(device, deviceAttr, graphValueChanged);
 
  }
 
    d1.set(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";
 
    if (this.effect) {
 
      const nv = this.effect.currentValue(device, deviceAttr);
 
      // log(`i have a a cb for ${device.value}-${deviceAttr.value}; start value is ${nv}`);
 
      graphValueChanged(nv);
 
  }
 

	
 
  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) {
 
      log("controlChanged, no 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);
 
      }
 
    const p = this.effect.edit(device, deviceAttr, value);
 
    this.graph.applyAndSendPatch(p);
 
    }
 
  }
 

	
 
  _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);
 
    }
 
  }
 
}
light9/web/live/Light9DeviceControl.ts
Show inline comments
 
@@ -10,13 +10,13 @@ import { GraphToControls } from "./Graph
 
import { Choice } from "./Light9Listbox";
 
import { Light9LiveControl } from "./Light9LiveControl";
 
export { ResourceDisplay } from "../ResourceDisplay";
 
export { Light9LiveControl };
 
const log = debug("devcontrol");
 

	
 
interface DeviceAttrRow {
 
export interface DeviceAttrRow {
 
  uri: NamedNode; //devattr
 
  attrClasses: string; // the css kind
 
  dataType: NamedNode;
 
  showColorPicker: boolean;
 
  useColor: boolean;
 
  useChoice: boolean;
 
@@ -105,21 +105,21 @@ export class Light9DeviceControl extends
 
  @property() effect!: NamedNode;
 
  @property() graphToControls!: GraphToControls;
 

	
 
  @property() devClasses: string = ""; // the css kind
 
  @property() deviceAttrs: DeviceAttrRow[] = [];
 
  @property() deviceClass: NamedNode | null = null;
 
  @property() selectedAttrs: Set<string> = new Set();
 
  @property() selectedAttrs: Set<NamedNode> = new Set();
 

	
 
  constructor() {
 
    super();
 
    getTopGraph().then((g) => {
 
      this.graph = g;
 
      this.graph.runHandler(this.configureFromGraphz.bind(this), `${this.uri.value} update`);
 
      this.graph.runHandler(this.syncDeviceAttrsFromGraph.bind(this), `${this.uri.value} update`);
 
    });
 
    this.selectedAttrs = new Set(); // uri strings
 
    this.selectedAttrs = new Set();
 
  }
 

	
 
  _bgStyle(deviceClass: NamedNode | null): string {
 
    if (!deviceClass) return "";
 
    let hash = 0;
 
    const u = deviceClass.value;
 
@@ -134,25 +134,30 @@ export class Light9DeviceControl extends
 
  setDeviceSelected(isSel: any) {
 
    this.devClasses = isSel ? "selected" : "";
 
  }
 

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

	
 
  configureFromGraphz(patch?: Patch) {
 
  syncDeviceAttrsFromGraph(patch?: Patch) {
 
    const U = this.graph.U();
 
    if (patch != null && !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();
 
  }
 
@@ -162,13 +167,13 @@ export class Light9DeviceControl extends
 
    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" : "",
 
      attrClasses: this.selectedAttrs.has(devAttr) ? "selected" : "",
 
      useColor: false,
 
      useChoice: false,
 
      choices: [] as Choice[],
 
      choiceSize: 0,
 
      useSlider: false,
 
      max: 1,
 
@@ -195,12 +200,15 @@ export class Light9DeviceControl extends
 
      }
 
    }
 
    return daRow;
 
  }
 

	
 
  clear() {
 
    // why can't we just set their values ? what's diff about
 
    // the clear state, and should it be represented with `null` value?
 
    throw new Error()
 
    Array.from(this.shadowRoot!.querySelectorAll("light9-live-control")).map((lc: Element) => (lc as Light9LiveControl).clear());
 
  }
 

	
 
  onClick(ev: any) {
 
    log("click", this.uri);
 
    // select, etc
light9/web/live/Light9LiveControl.ts
Show inline comments
 
import debug from "debug";
 
const log = debug("control");
 
import { css, html, LitElement } from "lit";
 
import { css, html, LitElement, PropertyPart, PropertyValues } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 
import { NamedNode } from "n3";
 
import { getTopGraph } from "../RdfdbSyncedGraph";
 
import { SyncedGraph } from "../SyncedGraph";
 

	
 
import { ControlValue } from "./Effect";
 
import { GraphToControls } from "./GraphToControls";
 
import { DeviceAttrRow } from "./Light9DeviceControl";
 
import { Choice } from "./Light9Listbox";
 
export { Slider } from "@material/mwc-slider";
 
@customElement("light9-live-control")
 
export class Light9LiveControl extends LitElement {
 
  graph!:SyncedGraph
 
  graph!: SyncedGraph;
 

	
 
  static styles = [
 
    css`
 
      #colorControls {
 
        display: flex;
 
        align-items: center;
 
@@ -37,113 +43,115 @@ export class Light9LiveControl extends L
 
        }
 
      }
 
    `,
 
  ];
 

	
 
  render() {
 
    if (this.dataType.value === "http://light9.bigasterisk.com/scalar") {
 
      return html`<mwc-slider .value=${this.sliderValue} step=${1 / 255} min="0" max="1" @input=${this.onSliderInput}></mwc-slider> `;
 
    } else if (this.dataType.value === "http://light9.bigasterisk.com/color") {
 
    return html`
 
      <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>
 
    `;
 
    } else if (this.dataType.value === "http://light9.bigasterisk.com/choice") {
 
      return html` <light9-listbox choices="{{deviceAttrRow.choices}}" value="{{choiceValue}}"> </light9-listbox> `;
 
    } else {
 
      throw new Error(`${this.dataType} unknown`);
 
    }
 
  }
 

	
 

	
 

	
 
  // passed from parent
 
  @property() device!: NamedNode;
 
  @property() dataType: NamedNode;
 
  @property() deviceAttrRow!: DeviceAttrRow;
 
  // we'll connect to this and receive graphValueChanged and send uiValueChanged
 
  @property() graphToControls!: GraphToControls;
 

	
 
  // "onChange(value)", 
 
  // "onChoice(choiceValue)"];
 
  // "onGraphToControls(graphToControls)", 
 
  // choiceValue: { type: any; };
 
  // choiceValue: { type: Object },
 
  // choiceValue: any;
 
  // device: { type: any; };
 
  // device: { type: Object },
 
  // deviceAttrRow: { type: any; }; // object returned from attrRow, below
 
  // deviceAttrRow: { type: Object }, // object returned from attrRow, below
 
  // deviceAttrRow: any;
 
  // enableChange: boolean;
 
  // graph: { type: Object, notify: true },
 
  // graphToControls: { ...; };
 
  // graphToControls: { type: Object },
 
  // graphToControls: any;
 
  // immediateSlider: { notify: boolean; observer: string; };
 
  // immediateSlider: { notify: true, observer: "onSlider" },
 
  // immediateSlider: any;
 
  // pickedChoice: { ...; };
 
  // pickedChoice: { observer: "onChange" },
 
  // pickedChoice: any;
 
  // sliderWriteValue: { ...; };
 
  // sliderWriteValue: { type: Number },
 
  // sliderWriteValue: { value: any; };
 
  // value: { type: any; notify: boolean; }; // null, Uri, float, str
 
  // value: { type: Object, notify: true }, // null, Uri, float, str
 
  // value: any;
 
  @property() enableChange: boolean = false;
 
  @property() value: ControlValue | null = null;
 

	
 
  // slider mode
 
  @property() sliderValue: number = 0;
 

	
 
  // color mode
 

	
 
  // choice mode
 
  @property() pickedChoice: Choice | null = null;
 
  @property() choiceValue: Choice | null = null;
 
  
 
  constructor() {
 
    super();
 
    this.enableChange = false; // until 1st graph read
 
  }
 
  onSlider() {
 
    return (this.value = this.immediateSlider);
 
    this.dataType = new NamedNode("http://light9.bigasterisk.com/scalar");
 
    // getTopGraph().then((g) => {
 
    //   this.graph = g;
 
    //   // this.graph.runHandler(this.graphReads.bind(this), `${this.device} ${this.deviceAttrRow.uri} reads`);
 
    // });
 
  }
 
  goBlack() {
 
    return (this.value = "#000000");
 

	
 
  // graphReads() {
 
  //   const U = this.graph.U();
 
  // }
 

	
 
  updated(changedProperties: PropertyValues) {
 
    if (changedProperties.has("graphToControls")) {
 
      this.graphToControls.register(this.device, this.deviceAttrRow.uri, this.onGraphValueChanged.bind(this));
 
      this.enableChange = true;
 
  }
 
  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.");
 

	
 
  onSliderInput(ev: CustomEvent) {
 
    if (ev.detail === undefined) {
 
      // not sure what this is, but it seems to be followed by good events
 
      return;
 
    }
 
    log(ev.type, ev.detail?.value);
 
    this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, ev.detail.value);
 
  }
 

	
 
  graphValueChanged(v: { value: any }) {
 
    log("change: control gets", v);
 
    this.enableChange = false;
 
    if (v === null) {
 
      this.clear();
 
  onGraphValueChanged(v: ControlValue | null) {
 
    // log("change: control must display", v);
 
    // this.enableChange = false;
 
    if (this.dataType.value == "http://light9.bigasterisk.com/scalar") {
 
      if (v !== null) {
 
        setTimeout(() => {
 
          // only needed once per page layout
 
          this.shadowRoot?.querySelector("mwc-slider")?.layout(/*skipUpdateUI=*/ false);
 
        }, 1);
 
        this.sliderValue = v as number;
 
    } else {
 
      this.value = v;
 
        this.sliderValue = 0;
 
      }
 
    }
 
    if (this.deviceAttrRow.useSlider) {
 
      this.sliderWriteValue = v;
 
    // if (v === null) {
 
    //   this.clear();
 
    // } else {
 
    //   this.value = v;
 
    // }
 
    // if (this.deviceAttrRow.useChoice) {
 
    //   this.choiceValue = v === null ? v : v.value;
 
    // }
 
    // this.enableChange = true;
 
    }
 
    if (this.deviceAttrRow.useChoice) {
 
      this.choiceValue = v === null ? v : v.value;
 
    }
 
    return (this.enableChange = true);
 

	
 
  goBlack() {
 
    this.value = "#000000";
 
  }
 

	
 
  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);
 
    this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, value);
 
  }
 

	
 
  onChange(value: any) {
 
    if (this.graphToControls == null || !this.enableChange) {
 
      return;
 
    }
 
@@ -151,21 +159,21 @@ export class Light9LiveControl extends L
 
      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);
 
    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);
 
  // 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.sliderValue = 0);
 
  //   }
 
  // }
 
    }
 
  }
 
}
light9/web/live/Light9LiveControls.ts
Show inline comments
 
import debug from "debug";
 
import { css, html, LitElement } from "lit";
 
import { customElement } from "lit/decorators.js";
 
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 { 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;
 

	
 
@@ -44,18 +44,17 @@ export class Light9LiveControls extends 
 
      <rdfdb-synced-graph></rdfdb-synced-graph>
 

	
 
      <h1>device control</h1>
 

	
 
      <div id="save">
 
        <div>
 
          <button on-click="newEffect">New effect</button>
 
          <button @click=${this.newEffect}>New effect</button>
 
          <edit-choice .uri=${this.effectChoice}></edit-choice>
 
          <button on-click="clearAll">clear settings in this effect</button>
 
          <button @click=${this.clearAll}>clear settings in this effect</button>
 
        </div>
 
      </div>
 

	
 
      <div id="deviceControls">
 
        ${this.devices.map(
 
          (device: NamedNode) => html`
 
            <light9-device-control .uri=${device} .effect=${this.effectChoice} .graphToControls=${this.graphToControls}></light9-device-control>
 
          `
 
        )}
 
@@ -63,103 +62,117 @@ export class Light9LiveControls extends 
 
    `;
 
  }
 

	
 
  devices: Array<NamedNode> = [];
 
  // uri of the effect being edited, or null. This is the
 
  // master value; GraphToControls follows.
 
  effectChoice: NamedNode | null = null;
 
  @property() effectChoice: NamedNode | null = null;
 
  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.bind(this);
 
      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();
 

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

	
 
  writeToUrl(effectStr: any) {
 
  writeToUrl(effect: NamedNode | null) {
 
    const effectStr = effect ? this.graph.shorten(effect) : "";
 
    if (!this.okToWriteUrl) {
 
      return;
 
    }
 
    const u = new URL(window.location.href);
 
    if (u.searchParams.get("effect") === effectStr) {
 
    if ((u.searchParams.get("effect") || "") === effectStr) {
 
      return;
 
    }
 
    u.searchParams.set("effect", effectStr);
 
    u.searchParams.set("effect", effectStr); // this escapes : and / and i wish it didn't
 
    window.history.replaceState({}, "", u.href);
 
    return log("wrote new url", u.href);
 
  }
 

	
 
  newEffect() {
 
    return (this.effectChoice = this.graphToControls.newEffect().value);
 
    this.effectChoice = this.graphToControls.newEffect();
 
  }
 

	
 
  onEffectChoice() {
 
    const U = (x: any) => this.graph.Uri(x);
 
    if (this.effectChoice == null) {
 
      // unlink
 
      log("onEffectChoice unlink");
 
      if (this.graphToControls != null) {
 
        this.graphToControls.setEffect(null);
 
      }
 
    } else {
 
      log("load", this.effectChoice);
 
      if (this.graphToControls != null) {
 
        this.graphToControls.setEffect(this.effectChoice);
 
      } else {
 
        throw new Error("graphToControls not set");
 
      }
 
    }
 
    return this.writeToUrl(this.effectChoice);
 
    this.writeToUrl(this.effectChoice);
 
  }
 

	
 
  clearAll() {
 
    // clears the effect!
 
    return this.graphToControls.emptyEffect();
 
  }
 

	
 
  configureFromGraph() {
 
    const U = (x: string) => this.graph.Uri(x);
 

	
 
    const newDevs = [];
 
    const newDevs: NamedNode[] = [];
 
    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 });
 
        if (newDevs.length == 0) newDevs.push(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);
 
    log("is this called?");
 
    log(`controls update now has ${newDevs.length} devices`);
 
    this.devices = newDevs;
 
    this.requestUpdate();
 

	
 
    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.
0 comments (0 inline, 0 general)