comparison 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
comparison
equal deleted inserted replaced
2375:623836db99af 2376:4556eebe5d73
1 import debug from "debug";
2 import { Literal, NamedNode, Quad, Quad_Object, Quad_Predicate, Quad_Subject, Term } from "n3";
3 import { some } from "underscore";
4 import { Patch } from "../patch";
5 import { SyncedGraph } from "../SyncedGraph";
6 import { shortShow } from "../show_specific";
7 import { SubEvent } from "sub-events";
8
9 // todo: Align these names with newtypes.py, which uses HexColor and VTUnion.
10 type Color = string;
11 export type ControlValue = number | Color | NamedNode;
12
13 const log = debug("effect");
14
15 function isUri(x: Term | number | string): x is NamedNode {
16 return typeof x == "object" && x.termType == "NamedNode";
17 }
18
19 // todo: eliminate this. address the scaling when we actually scale
20 // stuff, instead of making a mess of every setting
21 function valuePred(graph: SyncedGraph, attr: NamedNode): NamedNode {
22 const U = graph.U();
23 const scaledAttributeTypes = [U(":color"), U(":brightness"), U(":uv")];
24 if (some(scaledAttributeTypes, (x: NamedNode) => attr.equals(x))) {
25 return U(":value");
26 } else {
27 return U(":value");
28 }
29 }
30
31 // also see resourcedisplay's version of this
32 function effContext(graph: SyncedGraph, uri: NamedNode): NamedNode {
33 return graph.Uri(uri.value.replace("light9.bigasterisk.com/effect", `light9.bigasterisk.com/show/${shortShow}/effect`));
34 }
35
36 export function newEffect(graph: SyncedGraph): NamedNode {
37 // wrong- this should be our editor's scratch effect, promoted to a
38 // real one when you name it.
39 const uri = graph.nextNumberedResource(graph.Uri("http://light9.bigasterisk.com/effect/effect"));
40
41 const effect = new Effect(graph, uri);
42 const U = graph.U();
43 const ctx = effContext(graph, uri);
44 const quad = (s: Quad_Subject, p: Quad_Predicate, o: Quad_Object) => graph.Quad(s, p, o, ctx);
45
46 const addQuads = [
47 quad(uri, U("rdf:type"), U(":Effect")),
48 quad(uri, U("rdfs:label"), graph.Literal(uri.value.replace(/.*\//, ""))),
49 quad(uri, U(":publishAttr"), U(":strength")),
50 quad(uri, U(":effectFunction"), U(":effectFunction/scale")),
51 ];
52 const patch = new Patch([], addQuads);
53 log("init new effect", patch);
54 graph.applyAndSendPatch(patch);
55
56 return effect.uri;
57 }
58
59 // effect settings data; r/w sync with the graph
60 export class Effect {
61 // :effect1 a Effect; :setting ?eset . ?eset :effectAttr :deviceSettings; :value ?dset . ?dset :device ..
62 private eset?: NamedNode;
63 private dsettings: Array<{ dset: NamedNode; device: NamedNode; deviceAttr: NamedNode; value: ControlValue }> = [];
64
65 private ctxForEffect: NamedNode;
66 settingsChanged: SubEvent<void> = new SubEvent();
67
68 constructor(public graph: SyncedGraph, public uri: NamedNode) {
69 this.ctxForEffect = effContext(this.graph, this.uri);
70 graph.runHandler(this.rebuildSettingsFromGraph.bind(this), `effect sync ${uri.value}`);
71 }
72
73 private getExistingEset(): NamedNode | null {
74 const U = this.graph.U();
75 for (let eset of this.graph.objects(this.uri, U(":setting"))) {
76 if (this.graph.uriValue(eset as Quad_Subject, U(":effectAttr")).equals(U(":deviceSettings"))) {
77 return eset as NamedNode;
78 }
79 }
80 return null;
81 }
82 private getExistingEsetValueNode(): NamedNode | null {
83 const U = this.graph.U();
84 const eset = this.getExistingEset();
85 if (eset === null) return null;
86 try {
87 return this.graph.uriValue(eset, U(":value"));
88 } catch (e) {
89 return null;
90 }
91 }
92 private patchForANewEset(): { p: Patch; eset: NamedNode } {
93 const U = this.graph.U();
94 const eset = this.graph.nextNumberedResource(U(":e_set"));
95 return {
96 eset: eset,
97 p: new Patch(
98 [],
99 [
100 //
101 new Quad(this.uri, U(":setting"), eset, this.ctxForEffect),
102 new Quad(eset, U(":effectAttr"), U(":deviceSettings"), this.ctxForEffect),
103 ]
104 ),
105 };
106 }
107
108 private rebuildSettingsFromGraph(patch?: Patch) {
109 const U = this.graph.U();
110
111 log("syncFromGraph", this.uri);
112
113 // 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
114 const newSettings = [];
115
116 const deviceSettingsNode = this.getExistingEsetValueNode();
117 if (deviceSettingsNode !== null) {
118 for (let dset of Array.from(this.graph.objects(deviceSettingsNode, U(":setting"))) as NamedNode[]) {
119 // // log(` setting ${setting.value}`);
120 // if (!isUri(dset)) throw new Error();
121 let value: ControlValue;
122 const device = this.graph.uriValue(dset, U(":device"));
123 const deviceAttr = this.graph.uriValue(dset, U(":deviceAttr"));
124
125 const pred = valuePred(this.graph, deviceAttr);
126 try {
127 value = this.graph.uriValue(dset, pred);
128 if (!(value as NamedNode).id.match(/^http/)) {
129 throw new Error("not uri");
130 }
131 } catch (error) {
132 try {
133 value = this.graph.floatValue(dset, pred);
134 } catch (error1) {
135 value = this.graph.stringValue(dset, pred); // this may find multi values and throw
136 }
137 }
138 // log(`change: graph contains ${deviceAttr.value} ${value}`);
139
140 newSettings.push({ dset, device, deviceAttr, value });
141 }
142 }
143 this.dsettings = newSettings;
144 log(`settings is rebuilt to length ${this.dsettings.length}`);
145 this.settingsChanged.emit(); // maybe one emitter per dev+attr?
146 // this.onValuesChanged();
147 }
148
149 currentValue(device: NamedNode, deviceAttr: NamedNode): ControlValue | null {
150 for (let s of this.dsettings) {
151 if (device.equals(s.device) && deviceAttr.equals(s.deviceAttr)) {
152 return s.value;
153 }
154 }
155 return null;
156 }
157
158 // change this object now, but return the patch to be applied to the graph so it can be coalesced.
159 edit(device: NamedNode, deviceAttr: NamedNode, newValue: ControlValue | null): Patch {
160 log(`edit: value=${newValue}`);
161 let existingSetting: NamedNode | null = null;
162 let result = new Patch([], []);
163
164 for (let s of this.dsettings) {
165 if (device.equals(s.device) && deviceAttr.equals(s.deviceAttr)) {
166 if (existingSetting !== null) {
167 // 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.
168 log(`${this.uri.value} had two settings for ${device.value} - ${deviceAttr.value} - deleting ${s.dset}`);
169 result = result.update(this.removeEffectSetting(s.dset));
170 }
171 existingSetting = s.dset;
172 }
173 }
174
175 if (newValue !== null && this.shouldBeStored(deviceAttr, newValue)) {
176 if (existingSetting === null) {
177 result = result.update(this.addEffectSetting(device, deviceAttr, newValue));
178 } else {
179 result = result.update(this.patchExistingDevSetting(existingSetting, deviceAttr, newValue));
180 }
181 } else {
182 if (existingSetting !== null) {
183 result = result.update(this.removeEffectSetting(existingSetting));
184 }
185 }
186 return result;
187 }
188
189 shouldBeStored(deviceAttr: NamedNode, value: ControlValue | null): boolean {
190 // this is a bug for zoom=0, since collector will default it to
191 // stick at the last setting if we don't explicitly send the
192 // 0. rx/ry similar though not the exact same deal because of
193 // their remap.
194 return value != null && value !== 0 && value !== "#000000";
195 }
196
197 private addEffectSetting(device: NamedNode, deviceAttr: NamedNode, value: ControlValue): Patch {
198 log(" _addEffectSetting", deviceAttr.value, value);
199 const U = (x: string) => this.graph.Uri(x);
200 const quad = (s: Quad_Subject, p: Quad_Predicate, o: Quad_Object) => this.graph.Quad(s, p, o, this.ctxForEffect);
201
202 let patch = new Patch([], []);
203
204 let eset = this.getExistingEset();
205 if (eset === null) {
206 const ret = this.patchForANewEset();
207 patch = patch.update(ret.p);
208 eset = ret.eset;
209 }
210
211 let dsValue;
212 try {
213 dsValue = this.graph.uriValue(eset, U(":value"));
214 } catch (e) {
215 dsValue = this.graph.nextNumberedResource(U(":ds_val"));
216 patch = patch.update(new Patch([], [quad(eset, U(":value"), dsValue)]));
217 }
218
219 const dset = this.graph.nextNumberedResource(this.uri.value + "_set");
220
221 patch = patch.update(
222 new Patch(
223 [],
224 [
225 quad(dsValue, U(":setting"), dset),
226 quad(dset, U(":device"), device),
227 quad(dset, U(":deviceAttr"), deviceAttr),
228 quad(dset, valuePred(this.graph, deviceAttr), this.nodeForValue(value)),
229 ]
230 )
231 );
232 log(" save", patch);
233 this.dsettings.push({ dset, device, deviceAttr, value });
234 return patch;
235 }
236
237 private patchExistingDevSetting(devSetting: NamedNode, deviceAttr: NamedNode, value: ControlValue): Patch {
238 log(" patch existing", devSetting.value);
239 return this.graph.getObjectPatch(
240 devSetting, //
241 valuePred(this.graph, deviceAttr),
242 this.nodeForValue(value),
243 this.ctxForEffect
244 );
245 }
246
247 private removeEffectSetting(effectSetting: NamedNode): Patch {
248 const U = (x: string) => this.graph.Uri(x);
249 log(" _removeEffectSetting", effectSetting.value);
250
251 const eset = this.getExistingEset();
252 if (eset === null) throw "unexpected";
253 const dsValue = this.graph.uriValue(eset, U(":value"));
254 if (dsValue === null) throw "unexpected";
255 const toDel = [this.graph.Quad(dsValue, U(":setting"), effectSetting, this.ctxForEffect)];
256 for (let q of this.graph.subjectStatements(effectSetting)) {
257 toDel.push(q);
258 }
259 return new Patch(toDel, []);
260 }
261
262 clearAllSettings() {
263 for (let s of this.dsettings) {
264 this.graph.applyAndSendPatch(this.removeEffectSetting(s.dset));
265 }
266 }
267
268 private nodeForValue(value: ControlValue): NamedNode | Literal {
269 if (value === null) {
270 throw new Error("no value");
271 }
272 if (isUri(value)) {
273 return value;
274 }
275 return this.graph.prettyLiteral(value);
276 }
277 }