diff web/live/Effect.ts @ 2376:4556eebe5d73

topdir reorgs; let pdm have its src/ dir; separate vite area from light9/
author drewp@bigasterisk.com
date Sun, 12 May 2024 19:02:10 -0700
parents light9/web/live/Effect.ts@06bf6dae8e64
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/live/Effect.ts	Sun May 12 19:02:10 2024 -0700
@@ -0,0 +1,277 @@
+import debug from "debug";
+import { Literal, NamedNode, Quad, Quad_Object, Quad_Predicate, Quad_Subject, Term } from "n3";
+import { some } from "underscore";
+import { Patch } from "../patch";
+import { SyncedGraph } from "../SyncedGraph";
+import { shortShow } from "../show_specific";
+import { SubEvent } from "sub-events";
+
+// todo: Align these names with newtypes.py, which uses HexColor and VTUnion.
+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");
+  }
+}
+
+// also see resourcedisplay's version of this
+function effContext(graph: SyncedGraph, uri: NamedNode): NamedNode {
+  return graph.Uri(uri.value.replace("light9.bigasterisk.com/effect", `light9.bigasterisk.com/show/${shortShow}/effect`));
+}
+
+export function newEffect(graph: SyncedGraph): NamedNode {
+  // wrong- this should be our editor's scratch effect, promoted to a
+  // real one when you name it.
+  const uri = graph.nextNumberedResource(graph.Uri("http://light9.bigasterisk.com/effect/effect"));
+
+  const effect = new Effect(graph, uri);
+  const U = graph.U();
+  const ctx = effContext(graph, uri);
+  const quad = (s: Quad_Subject, p: Quad_Predicate, o: Quad_Object) => graph.Quad(s, p, o, ctx);
+
+  const addQuads = [
+    quad(uri, U("rdf:type"), U(":Effect")),
+    quad(uri, U("rdfs:label"), graph.Literal(uri.value.replace(/.*\//, ""))),
+    quad(uri, U(":publishAttr"), U(":strength")),
+    quad(uri, U(":effectFunction"), U(":effectFunction/scale")),
+  ];
+  const patch = new Patch([], addQuads);
+  log("init new effect", patch);
+  graph.applyAndSendPatch(patch);
+
+  return effect.uri;
+}
+
+// effect settings data; r/w sync with the graph
+export class Effect {
+  // :effect1 a Effect; :setting ?eset . ?eset :effectAttr :deviceSettings; :value ?dset . ?dset :device ..
+  private eset?: NamedNode;
+  private dsettings: Array<{ dset: NamedNode; device: NamedNode; deviceAttr: NamedNode; value: ControlValue }> = [];
+
+  private ctxForEffect: NamedNode;
+  settingsChanged: SubEvent<void> = new SubEvent();
+
+  constructor(public graph: SyncedGraph, public uri: NamedNode) {
+    this.ctxForEffect = effContext(this.graph, this.uri);
+    graph.runHandler(this.rebuildSettingsFromGraph.bind(this), `effect sync ${uri.value}`);
+  }
+
+  private getExistingEset(): NamedNode | null {
+    const U = this.graph.U();
+    for (let eset of this.graph.objects(this.uri, U(":setting"))) {
+      if (this.graph.uriValue(eset as Quad_Subject, U(":effectAttr")).equals(U(":deviceSettings"))) {
+        return eset as NamedNode;
+      }
+    }
+    return null;
+  }
+  private getExistingEsetValueNode(): NamedNode | null {
+    const U = this.graph.U();
+    const eset = this.getExistingEset();
+    if (eset === null) return null;
+    try {
+      return this.graph.uriValue(eset, U(":value"));
+    } catch (e) {
+      return null;
+    }
+  }
+  private patchForANewEset(): { p: Patch; eset: NamedNode } {
+    const U = this.graph.U();
+    const eset = this.graph.nextNumberedResource(U(":e_set"));
+    return {
+      eset: eset,
+      p: new Patch(
+        [],
+        [
+          //
+          new Quad(this.uri, U(":setting"), eset, this.ctxForEffect),
+          new Quad(eset, U(":effectAttr"), U(":deviceSettings"), this.ctxForEffect),
+        ]
+      ),
+    };
+  }
+
+  private rebuildSettingsFromGraph(patch?: Patch) {
+    const U = this.graph.U();
+
+    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 deviceSettingsNode = this.getExistingEsetValueNode();
+    if (deviceSettingsNode !== null) {
+      for (let dset of Array.from(this.graph.objects(deviceSettingsNode, U(":setting"))) as NamedNode[]) {
+        //   // log(`  setting ${setting.value}`);
+        //   if (!isUri(dset)) throw new Error();
+        let value: ControlValue;
+        const device = this.graph.uriValue(dset, U(":device"));
+        const deviceAttr = this.graph.uriValue(dset, U(":deviceAttr"));
+
+        const pred = valuePred(this.graph, deviceAttr);
+        try {
+          value = this.graph.uriValue(dset, pred);
+          if (!(value as NamedNode).id.match(/^http/)) {
+            throw new Error("not uri");
+          }
+        } catch (error) {
+          try {
+            value = this.graph.floatValue(dset, pred);
+          } catch (error1) {
+            value = this.graph.stringValue(dset, pred); // this may find multi values and throw
+          }
+        }
+        //   log(`change: graph contains ${deviceAttr.value} ${value}`);
+
+        newSettings.push({ dset, device, deviceAttr, value });
+      }
+    }
+    this.dsettings = newSettings;
+    log(`settings is rebuilt to length ${this.dsettings.length}`);
+    this.settingsChanged.emit(); // maybe one emitter per dev+attr?
+    // this.onValuesChanged();
+  }
+
+  currentValue(device: NamedNode, deviceAttr: NamedNode): ControlValue | null {
+    for (let s of this.dsettings) {
+      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 = new Patch([], []);
+
+    for (let s of this.dsettings) {
+      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.dset}`);
+          result = result.update(this.removeEffectSetting(s.dset));
+        }
+        existingSetting = s.dset;
+      }
+    }
+
+    if (newValue !== null && this.shouldBeStored(deviceAttr, newValue)) {
+      if (existingSetting === null) {
+        result = result.update(this.addEffectSetting(device, deviceAttr, newValue));
+      } else {
+        result = result.update(this.patchExistingDevSetting(existingSetting, deviceAttr, newValue));
+      }
+    } else {
+      if (existingSetting !== null) {
+        result = result.update(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.
+    return value != null && value !== 0 && value !== "#000000";
+  }
+
+  private addEffectSetting(device: NamedNode, deviceAttr: NamedNode, value: ControlValue): Patch {
+    log("  _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.ctxForEffect);
+
+    let patch = new Patch([], []);
+
+    let eset = this.getExistingEset();
+    if (eset === null) {
+      const ret = this.patchForANewEset();
+      patch = patch.update(ret.p);
+      eset = ret.eset;
+    }
+
+    let dsValue;
+    try {
+      dsValue = this.graph.uriValue(eset, U(":value"));
+    } catch (e) {
+      dsValue = this.graph.nextNumberedResource(U(":ds_val"));
+      patch = patch.update(new Patch([], [quad(eset, U(":value"), dsValue)]));
+    }
+
+    const dset = this.graph.nextNumberedResource(this.uri.value + "_set");
+
+    patch = patch.update(
+      new Patch(
+        [],
+        [
+          quad(dsValue, U(":setting"), dset),
+          quad(dset, U(":device"), device),
+          quad(dset, U(":deviceAttr"), deviceAttr),
+          quad(dset, valuePred(this.graph, deviceAttr), this.nodeForValue(value)),
+        ]
+      )
+    );
+    log("  save", patch);
+    this.dsettings.push({ dset, device, deviceAttr, value });
+    return patch;
+  }
+
+  private patchExistingDevSetting(devSetting: NamedNode, deviceAttr: NamedNode, value: ControlValue): Patch {
+    log("  patch existing", devSetting.value);
+    return this.graph.getObjectPatch(
+      devSetting, //
+      valuePred(this.graph, deviceAttr),
+      this.nodeForValue(value),
+      this.ctxForEffect
+    );
+  }
+
+  private removeEffectSetting(effectSetting: NamedNode): Patch {
+    const U = (x: string) => this.graph.Uri(x);
+    log("  _removeEffectSetting", effectSetting.value);
+
+    const eset = this.getExistingEset();
+    if (eset === null) throw "unexpected";
+    const dsValue = this.graph.uriValue(eset, U(":value"));
+    if (dsValue === null) throw "unexpected";
+    const toDel = [this.graph.Quad(dsValue, U(":setting"), effectSetting, this.ctxForEffect)];
+    for (let q of this.graph.subjectStatements(effectSetting)) {
+      toDel.push(q);
+    }
+    return new Patch(toDel, []);
+  }
+
+  clearAllSettings() {
+    for (let s of this.dsettings) {
+      this.graph.applyAndSendPatch(this.removeEffectSetting(s.dset));
+    }
+  }
+
+  private nodeForValue(value: ControlValue): NamedNode | Literal {
+    if (value === null) {
+      throw new Error("no value");
+    }
+    if (isUri(value)) {
+      return value;
+    }
+    return this.graph.prettyLiteral(value);
+  }
+}