Changeset - 75dfd7eb1e77
[Not reviewed]
default
0 4 0
drewp@bigasterisk.com - 20 months ago 2023-05-27 09:05:27
drewp@bigasterisk.com
clean up. still seems to work
4 files changed with 38 insertions and 196 deletions:
0 comments (0 inline, 0 general)
light9/live/Effect.ts
Show inline comments
 
@@ -10,140 +10,135 @@ import { SubEvent } from "sub-events";
 
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";
 
}
 

	
 
// todo: eliminate this. address the scaling when we actually scale
 
// stuff, instead of making a mess of every setting
 
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(":value");
 
  } 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 }> = [];
 
  private ctxForEffect: NamedNode
 
  settingsChanged:SubEvent<void>=new SubEvent()
 
  private ctxForEffect: NamedNode;
 
  settingsChanged: SubEvent<void> = new SubEvent();
 
  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
 
    public uri: NamedNode // called if the graph changes our values and not when the caller uses edit()
 
  ) {
 
    this.ctxForEffect = this.graph.Uri(this.uri.value.replace("light9.bigasterisk.com/effect", `light9.bigasterisk.com/show/${shortShow}/effect`));
 
    graph.runHandler(this.rebuildSettingsFromGraph.bind(this), `effect sync ${uri.value}`);
 
  }
 

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

	
 
    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(patch?: Patch) {
 
    const U = this.graph.U();
 
    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);
 
    // log("syncFromGraph", this.uri);
 

	
 
    // this repeats work- it gathers all settings when really some values changed (and we might even know about them). maybe push the value-fetching into a secnod phase of the run, and have the 1st phase drop out early
 
    const newSettings = [];
 

	
 
    const seenDevAttrPairs: Set<string> = new Set();
 

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

	
 
      newSettings.push({ device, deviceAttr, setting, value });
 
    }
 
    log(newSettings)
 
    this.settings = newSettings;
 
    log(`rebuild to ${this.settings.length}`);
 
    this.settingsChanged.emit()
 
    this.settingsChanged.emit(); // maybe one emitter per dev+attr?
 
    // 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;
 
    let result = { adds: [], dels: [] };
 
    for (let s of this.settings) {
 
      if (device.equals(s.device) && deviceAttr.equals(s.deviceAttr)) {
 
        if (existingSetting !== null) {
 
          // this is corrupt. There was only supposed to be one setting per (dev,attr) pair. But we can fix it because we're going to update existingSetting to the user's requested value.
 
          log(`${this.uri.value} had two settings for ${device.value} - ${deviceAttr.value} - deleting ${s.setting}`);
 
          patchUpdate(result,  this._removeEffectSetting(s.setting));
 
          patchUpdate(result, this._removeEffectSetting(s.setting));
 
        }
 
        existingSetting = s.setting;
 
      }
 
    }
 

	
 
    if (newValue !== null && this.shouldBeStored(deviceAttr, newValue)) {
 
      if (existingSetting === null) {
 
        patchUpdate(result, this._addEffectSetting(device, deviceAttr, newValue));
 
      } else {
 
        patchUpdate(result, this._patchExistingEffectSetting(existingSetting, deviceAttr, newValue));
 
      }
 
    } else {
 
      if (existingSetting !== null) {
 
        patchUpdate(result, this._removeEffectSetting(existingSetting));
 
      }
 
    }
 
    return result;
 
  }
 

	
 
  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.
light9/live/Light9AttrControl.ts
Show inline comments
 
import debug from "debug";
 
import { css, html, LitElement, PropertyValues } from "lit";
 
import { customElement, property, state } from "lit/decorators.js";
 
import { Literal, NamedNode } from "n3";
 
import { SubEvent } from "sub-events";
 
import { getTopGraph } from "../web/RdfdbSyncedGraph";
 
import { SyncedGraph } from "../web/SyncedGraph";
 
import { ControlValue, Effect } from "./Effect";
 
import { GraphToControls } from "./GraphToControls";
 
import { DeviceAttrRow } from "./Light9DeviceControl";
 
import { Choice } from "./Light9Listbox";
 
import { getTopGraph } from "../web/RdfdbSyncedGraph";
 
export { Slider } from "@material/mwc-slider";
 
export { Light9ColorPicker } from "../web/light9-color-picker";
 

	
 
export { Light9Listbox } from "./Light9Listbox";
 
const log = debug("settings.dev.attr");
 

	
 
type DataTypeNames = "scalar" | "color" | "choice";
 
const makeType = (d: DataTypeNames) => new NamedNode(`http://light9.bigasterisk.com/${d}`);
 

	
 
// UI for one device attr (of any type).
 
@customElement("light9-attr-control")
 
export class Light9AttrControl extends LitElement {
 
  graph!: SyncedGraph;
 

	
 
  static styles = [
 
    css`
 
      #colorControls {
 
        display: flex;
 
        align-items: center;
 
      }
 
      #colorControls > * {
 
        margin: 0 3px;
 
      }
 
      :host {
 
        border: 2px solid white;
 
      }
 
      mwc-slider {
 
        width: 250px;
 
      }
 
    `,
 
  ];
 

	
 
  @property() deviceAttrRow: DeviceAttrRow | null = null;
 
  @state() dataType: DataTypeNames = "scalar";
 

	
 
  @property() effect: Effect | null = null;
 
  // we'll connect to this and receive graphValueChanged and send uiValueChanged
 
  // @property() graphToControls!: GraphToControls;
 

	
 
  @property() enableChange: boolean = false;
 
  @property() value: ControlValue | null = null; // e.g. color string
 

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

	
 
  // color mode
 

	
 
  // choice mode
 
  // @property() pickedChoice: Choice | null = null;
 
  // @property() choiceValue: Choice | null = null;
 

	
 
  valueChanged: SubEvent<Literal> = new SubEvent();
 

	
 
  constructor() {
 
    super();
 
    getTopGraph().then((g) => {
 
      this.graph = g;
 
      if (this.deviceAttrRow === null) throw new Error();
 
      // this.graph.runHandler(this.graphReads.bind(this), `${this.deviceAttrRow.device} ${this.deviceAttrRow.uri} reads`);
 
    });
 
  }
 

	
 
  connectedCallback(): void {
 
    super.connectedCallback();
 
    setTimeout(() => {
 
      // only needed once per page layout
 
      this.shadowRoot?.querySelector("mwc-slider")?.layout(/*skipUpdateUI=*/ false);
 
    }, 1);
 
  }
 

	
 
  render() {
 
    if (this.deviceAttrRow === null) throw new Error();
 
    const dbg = html` live-control ${this.dataType}  ]`;
 
    if (this.dataType == "scalar") {
 
      const v = this.value || 0;
 
      return html`${dbg} <mwc-slider .value=${v} step=${1 / 255} min="0" max="1" @input=${this.onSliderInput}></mwc-slider> `;
 
      return html`<mwc-slider .value=${v} step=${1 / 255} min="0" max="1" @input=${this.onValueInput}></mwc-slider> `;
 
    } else if ((this.dataType = "color")) {
 
      const v = this.value || '#000'
 
      const v = this.value || "#000";
 
      return html`
 
        ${dbg}
 
        <div id="colorControls">
 
          <button on-click="goBlack">0.0</button>
 
          <light9-color-picker .color=${v} @input=${this.onColorInput}></light9-color-picker>
 
          <button @click=${this.goBlack}>0.0</button>
 
          <light9-color-picker .color=${v} @input=${this.onValueInput}></light9-color-picker>
 
        </div>
 
      `;
 
    } else if (this.dataType == "choice") {
 
      return html`${dbg} <light9-listbox .choices=${this.deviceAttrRow.choices} .value=${this.choiceValue}> </light9-listbox> `;
 
      return html`<light9-listbox .choices=${this.deviceAttrRow.choices} .value=${this.value}> </light9-listbox> `;
 
    }
 
  }
 

	
 
  // graphReads() {
 
  //   if (this.deviceAttrRow === null) throw new Error();
 
  //   const U = this.graph.U();
 
  //   this.effect?.currentValue(this.deviceAttrRow.device, this.deviceAttrRow.uri);
 
  // }
 

	
 
  updated(changedProperties: PropertyValues<this>) {
 
    super.updated(changedProperties);
 
    // if (changedProperties.has("graphToControls")) {
 
    //   // this.graphToControls.register(this.device, this.deviceAttrRow.uri, this.onGraphValueChanged.bind(this));
 
    //   this.enableChange = true;
 
    // }
 

	
 
    if (changedProperties.has("deviceAttrRow")) {
 
      this.onDeviceAttrRowProperty();
 
    }
 
    if (changedProperties.has("effect")) {
 
      this.onEffectProperty();
 
    }
 
    if (changedProperties.has("value")) {
 
      this.onValueProperty();
 
    }
 
  }
 

	
 
  private onValueProperty() {
 
    if (this.deviceAttrRow === null) throw new Error();
 
    if (this.effect !== null && this.graph !== undefined) {
 
      const p = this.effect.edit(this.deviceAttrRow.device, this.deviceAttrRow.uri, this.value);
 
      log("patch", p, "to", this.graph);
 
      if (p.adds.length || p.dels.length) {
 
        log("Effect told us to graph.patch", p, "to", this.graph);
 
        this.graph.applyAndSendPatch(p);
 
      }
 
    }
 
  }
 

	
 
  private onEffectProperty() {
 
    if (this.effect === null) throw new Error();
 
    // effect will read graph changes on its own, but emit an event when it does
 
    this.effect.settingsChanged.subscribe(() => {
 
      this.effectSettingsChanged();
 
    });
 
    this.effectSettingsChanged();
 
  }
 

	
 
  private effectSettingsChanged() {
 
    // anything in the settings graph is new
 
    log("i check the effect current value");
 
    // something in the settings graph is new
 
    if (this.deviceAttrRow === null) throw new Error();
 
    if (this.effect === null) throw new Error();
 
    log("graph->ui on ", this.deviceAttrRow.device, this.deviceAttrRow.uri);
 
    const v=this.effect.currentValue(this.deviceAttrRow.device, this.deviceAttrRow.uri);
 
    // log("graph->ui on ", this.deviceAttrRow.device, this.deviceAttrRow.uri);
 
    const v = this.effect.currentValue(this.deviceAttrRow.device, this.deviceAttrRow.uri);
 
    this.onGraphValueChanged(v);
 
  }
 

	
 
  private onDeviceAttrRowProperty() {
 
    if (this.deviceAttrRow === null) throw new Error();
 
    const d = this.deviceAttrRow.dataType;
 
    if (d.equals(makeType("scalar"))) {
 
      this.dataType = "scalar";
 
    } else if (d.equals(makeType("color"))) {
 
      this.dataType = "color";
 
    } else if (d.equals(makeType("choice"))) {
 
      this.dataType = "choice";
 
    }
 
  }
 

	
 
  onSliderInput(ev: CustomEvent) {
 
  onValueInput(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.value = ev.detail.value;
 
    // this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, ev.detail.value);
 
  }
 

	
 
onColorInput(ev: CustomEvent) {
 
  this.value = ev.detail.value;
 
}
 

	
 
  onGraphValueChanged(v: ControlValue | null) {
 
    if (this.deviceAttrRow === null) throw new Error();
 
    log("change: control must display", v, "for", this.deviceAttrRow.device.value, this.deviceAttrRow.uri.value);
 
    // log("change: control must display", v, "for", this.deviceAttrRow.device.value, this.deviceAttrRow.uri.value);
 
    // this.enableChange = false;
 
    if (this.dataType == "scalar") {
 
      if (v !== null) {
 
        setTimeout(() => {
 
          // only needed once per page layout
 
          this.shadowRoot?.querySelector("mwc-slider")?.layout(/*skipUpdateUI=*/ false);
 
        }, 1);
 
        this.value = v;
 
      } else {
 
         this.value = 0;
 
        this.value = 0;
 
      }
 
    } else if (this.dataType == "color") {
 
      log('graph sets coolor', v)
 
      this.value=v;
 
      this.value = v;
 
    }
 
    // if (v === null) {
 
    //   this.clear();
 
    // } else {
 
    //   this.value = v;
 
    // }
 
    // if (this.deviceAttrRow.useChoice) {
 
    //   this.choiceValue = v === null ? v : v.value;
 
    // }
 
    // this.enableChange = true;
 
  }
 

	
 
  graphToColor(v: ControlValue | null) {
 
    this.value = v === null ? "#000" : v;
 
    return;
 
    // const cp = this.shadowRoot?.querySelector("light9-color-picker") as Light9ColorPicker | null;
 
    // if (cp) {
 
    //   if (typeof v != "string") throw new Error("type v is " + typeof v);
 
    //   if (v === null) {
 
    //     v = "#000";
 
    //   }
 
    //   cp.setColor(v as string);
 
    // }
 
  }
 

	
 
  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;
 
    // }
 
    // 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;
 
    // }
 
    // 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.sliderValue = 0);
 
  //   }
 
  // }
 
}
light9/live/Light9DeviceControl.ts
Show inline comments
 
import debug from "debug";
 
import { css, html, LitElement } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 
import { NamedNode } from "n3";
 
import { unique } from "underscore";
 
import { Patch, patchContainsPreds } from "../web/patch";
 
import { getTopGraph } from "../web/RdfdbSyncedGraph";
 
import { SyncedGraph } from "../web/SyncedGraph";
 
import { GraphToControls } from "./GraphToControls";
 
import { Choice } from "./Light9Listbox";
 
import { Light9AttrControl } from "./Light9AttrControl";
 
import { Effect } from "./Effect";
 
export { ResourceDisplay } from "../web/ResourceDisplay";
 
export { Light9AttrControl };
 
const log = debug("settings.dev");
 

	
 
export interface DeviceAttrRow {
 
  uri: NamedNode; //devattr
 
  device: NamedNode;
 
  attrClasses: string; // the css kind
 
  dataType: NamedNode;
 
  showColorPicker: boolean;
 
  useColor: boolean;
 
  useChoice: boolean;
 
  choices: Choice[];
 
  choiceSize: number;
 
  useSlider: boolean;
 
  max: number;
 
  // choiceSize: number;
 
  // max: number;
 
}
 

	
 
// Widgets for one device with multiple Light9LiveControl rows for the attr(s).
 
@customElement("light9-device-control")
 
export class Light9DeviceControl extends LitElement {
 
  graph!: SyncedGraph;
 
  static styles = [
 
    css`
 
      :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;
 
      }
 
@@ -62,163 +57,152 @@ export class Light9DeviceControl extends
 
        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;
 
      }
 
    `,
 
  ];
 

	
 
  render() {
 
    return html`
 
      <div class="device ${this.devClasses}">
 
        <h2 style="${this._bgStyle(this.deviceClass)}" xon-click="onClick">
 
        <h2 style="${this._bgStyle(this.deviceClass)}" @click=${this.onClick}>
 
          <resource-display id="mainLabel" .uri="${this.uri}"></resource-display>
 
          a <resource-display minor .uri="${this.deviceClass}"></resource-display>
 
        </h2>
 

	
 
        ${this.deviceAttrs.map(
 
          (dattr: DeviceAttrRow) => html`
 
            <div @click="onAttrClick" class="deviceAttr ${dattr.attrClasses}">
 
              <span>
 
                attr
 
                <resource-display minor .uri=${dattr.uri}></resource-display>
 
              </span>
 
              <light9-attr-control .deviceAttrRow=${dattr} .effect=${this.effect}>
 
                .graphToControls={this.graphToControls}
 
              </light9-attr-control>
 
            </div>
 
          `
 
        )}
 
      </div>
 
    `;
 
  }
 

	
 
  @property() uri!: NamedNode;
 
  @property() effect!: Effect;
 
  // @property() graphToControls!: GraphToControls;
 

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

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

	
 
  _bgStyle(deviceClass: NamedNode | null): string {
 
    if (!deviceClass) return "";
 
    let hash = 0;
 
    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 && !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,
 
      device: this.uri,
 
      dataType,
 
      showColorPicker: dataType.equals(U(":color")),
 
      attrClasses: this.selectedAttrs.has(devAttr) ? "selected" : "",
 
      useColor: false,
 
      useChoice: false,
 
      choices: [] as Choice[],
 
      choiceSize: 0,
 
      useSlider: false,
 
      max: 1,
 
    };
 
    if (dataType.equals(U(":color"))) {
 
      daRow.useColor = true;
 
    } else if (dataType.equals(U(":choice"))) {
 
      daRow.useChoice = true;
 
     if (dataType.equals(U(":choice"))) {
 
      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() {
 
    // 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
 
  }
 

	
 
  onAttrClick(ev: { model: { dattr: { uri: any } } }) {
 
    log("attr click", this.uri, ev.model.dattr.uri);
 
    // select
light9/live/Light9DeviceSettings.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, patchContainsPreds } from "../web/patch";
 
import { Patch } from "../web/patch";
 
import { getTopGraph } from "../web/RdfdbSyncedGraph";
 
import { SyncedGraph } from "../web/SyncedGraph";
 
import { GraphToControls } from "./GraphToControls";
 
import { Effect } from "./Effect";
 
export { EditChoice } from "../web/EditChoice";
 
export { Light9DeviceControl as Light9LiveDeviceControl } from "./Light9DeviceControl";
 
const log = debug("settings");
 

	
 
@customElement("light9-device-settings")
 
export class Light9DeviceSettings extends LitElement {
 
  graph!: SyncedGraph;
 

	
 
  static styles = [
 
    css`
 
      :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 {
 
      light9-device-control > div {
 
        break-inside: avoid-column;
 
      }
 
      light9-live-device-control {
 
      light9-device-control {
 
        vertical-align: top;
 
      }
 
    `,
 
  ];
 

	
 
  render() {
 
    return html`
 
      <rdfdb-synced-graph></rdfdb-synced-graph>
 

	
 
      <h1>effect DeviceSettings</h1>
 

	
 
      <div id="save">
 
        <div>
 
          <button @click=${this.newEffect}>New effect</button>
 
          <edit-choice .uri=${this.currentEffect ? this.currentEffect.uri : null} @edited=${this.onEffectChoice2}></edit-choice>
 
          <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.currentEffect}> .graphToControls={this.graphToControls} </light9-device-control>
 
          `
 
        )}
 
      </div>
 
    `;
 
  }
 

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

	
 
  onEffectChoice2(ev: CustomEvent) {
 
    const uri = ev.detail.newValue as NamedNode;
 
    if (uri === null) {
 
      this.currentEffect = null;
 
    } else {
 
      this.currentEffect = new Effect(this.graph, uri);
 
    }
 
  }
 

	
 
  updated(changedProperties: PropertyValues<this>) {
 
    log("ctls udpated", changedProperties);
 
    if (changedProperties.has("currentEffect")) {
 
      log(`effectChoice to ${this.currentEffect?.uri?.value}`);
 
      this.onEffectChoice();
 
      this.writeToUrl(this.currentEffect?.uri);
 
    }
 
    // this.graphToControls?.debugDump();
 
  }
 

	
 
  // Note that this doesn't fetch setting values, so it only should get rerun
 
  // upon (rarer) changes to the devices etc.
 
  // upon (rarer) changes to the devices etc. todo: make that be true
 
  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) => {
 
        log(`found dev ${dev.value}`);
 
        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.currentEffect = new Effect(this.graph, this.graph.Uri(effect));
 
    }
 
    this.okToWriteUrl = true;
 
  }
 

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

	
 
  newEffect() {
 
    // 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 {
 
    //   if (this.graphToControls != null) {
 
    //     this.graphToControls.setEffect(this.effectChoice);
 
    //   } else {
 
    //     throw new Error("graphToControls not set");
 
    //   }
 
    // }
 
    this.writeToUrl(this.currentEffect?.uri);
 
  }
 

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

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

	
 
  //   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;
 
  //       }
 
  //       if (newDevs.length == 0) newDevs.push(dev);
 
  //     }
 
  //   }
 
  //   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.
 

	
 
  //   // 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
 
  //   );
 
  // }
 
}
0 comments (0 inline, 0 general)