Mercurial > code > home > repos > light9
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); + } +}