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