@kubohiroya/turbowarp-asset-cache 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/dist/asset-cache.js +911 -148
- package/dist/composition.js +934 -148
- package/dist/types/audio-output.d.ts +5 -0
- package/dist/types/audio-playback.d.ts +46 -0
- package/dist/types/composition.d.ts +3 -0
- package/dist/types/extension.d.ts +7 -0
- package/dist/types/fixed-input-editors.d.ts +10 -0
- package/dist/types/fixed-input-schema.d.ts +46 -0
- package/docs/audio-playback.md +49 -0
- package/docs/fixed-input-editors.ja.md +20 -0
- package/package.json +10 -6
- package/schemas/block-editor-fixed-inputs.schema.json +24 -0
package/dist/composition.js
CHANGED
|
@@ -1,3 +1,418 @@
|
|
|
1
|
+
//#region src/audio-output.ts
|
|
2
|
+
/** Media volume handles attenuation and fades; this shared graph adds amplification. */
|
|
3
|
+
var shared;
|
|
4
|
+
var maximumFloat = 34028234663852886e22;
|
|
5
|
+
function createAudioAmplifier(audio) {
|
|
6
|
+
if (typeof globalThis.AudioContext !== "function") throw new Error("Audio amplification requires Web Audio support.");
|
|
7
|
+
if (!shared || shared.context.state === "closed") shared = {
|
|
8
|
+
context: new AudioContext(),
|
|
9
|
+
users: 0
|
|
10
|
+
};
|
|
11
|
+
const owner = shared;
|
|
12
|
+
owner.users++;
|
|
13
|
+
let source;
|
|
14
|
+
let gain;
|
|
15
|
+
let released = false;
|
|
16
|
+
const dispose = () => {
|
|
17
|
+
if (released) return;
|
|
18
|
+
released = true;
|
|
19
|
+
source?.disconnect();
|
|
20
|
+
gain?.disconnect();
|
|
21
|
+
owner.users--;
|
|
22
|
+
if (owner.users === 0) {
|
|
23
|
+
if (shared === owner) shared = void 0;
|
|
24
|
+
owner.context.close().catch(() => {});
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
try {
|
|
28
|
+
source = owner.context.createMediaElementSource(audio);
|
|
29
|
+
gain = owner.context.createGain();
|
|
30
|
+
source.connect(gain);
|
|
31
|
+
gain.connect(owner.context.destination);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
dispose();
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
const node = gain;
|
|
37
|
+
return {
|
|
38
|
+
setGain(value) {
|
|
39
|
+
node.gain.value = Math.min(maximumFloat, Math.max(1, value));
|
|
40
|
+
},
|
|
41
|
+
resume() {
|
|
42
|
+
return owner.context.state === "running" ? Promise.resolve() : owner.context.resume();
|
|
43
|
+
},
|
|
44
|
+
dispose
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/audio-playback.ts
|
|
49
|
+
function playbackGain(value) {
|
|
50
|
+
const gain = value === void 0 ? 1 : value;
|
|
51
|
+
if (typeof gain !== "number" || !Number.isFinite(gain) || gain < 0) throw new TypeError("Audio gain must be a finite non-negative number.");
|
|
52
|
+
return gain;
|
|
53
|
+
}
|
|
54
|
+
function rate(value) {
|
|
55
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < .25 || value > 4) throw new TypeError("Audio playback rate must be from 0.25 to 4.");
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
function validateAudioPlaybackOptions(options) {
|
|
59
|
+
if (!options || typeof options !== "object" || Array.isArray(options) || Object.keys(options).some((key) => ![
|
|
60
|
+
"gain",
|
|
61
|
+
"muted",
|
|
62
|
+
"playbackRate",
|
|
63
|
+
"fadeIn",
|
|
64
|
+
"fadeOut"
|
|
65
|
+
].includes(key))) throw new TypeError("Invalid audio playback options.");
|
|
66
|
+
playbackGain(options.gain);
|
|
67
|
+
if (options.muted !== void 0 && typeof options.muted !== "boolean") throw new TypeError("Audio muted must be a boolean.");
|
|
68
|
+
rate(options.playbackRate ?? 1);
|
|
69
|
+
fadeSeconds(options.fadeIn, "fadeIn");
|
|
70
|
+
fadeSeconds(options.fadeOut, "fadeOut");
|
|
71
|
+
}
|
|
72
|
+
function fadeSeconds(value, label) {
|
|
73
|
+
const seconds = value === void 0 ? 0 : value;
|
|
74
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 0) throw new TypeError(`Audio ${label} must be a finite non-negative number of seconds.`);
|
|
75
|
+
return seconds;
|
|
76
|
+
}
|
|
77
|
+
function aborted() {
|
|
78
|
+
const error = /* @__PURE__ */ new Error("Audio playback was cancelled.");
|
|
79
|
+
error.name = "AbortError";
|
|
80
|
+
return error;
|
|
81
|
+
}
|
|
82
|
+
/** One reusable native element; the caller owns source resolution and resource release. */
|
|
83
|
+
function createBrowserAudioPlayback(audio, options, release, playbackError) {
|
|
84
|
+
validateAudioPlaybackOptions(options);
|
|
85
|
+
let gain = playbackGain(options.gain);
|
|
86
|
+
audio.volume = Math.min(1, gain);
|
|
87
|
+
audio.muted = options.muted ?? false;
|
|
88
|
+
audio.playbackRate = options.playbackRate ?? 1;
|
|
89
|
+
audio.loop = false;
|
|
90
|
+
const defaultFadeIn = fadeSeconds(options.fadeIn, "fadeIn");
|
|
91
|
+
const defaultFadeOut = fadeSeconds(options.fadeOut, "fadeOut");
|
|
92
|
+
let activeFadeIn = defaultFadeIn;
|
|
93
|
+
let activeFadeOut = defaultFadeOut;
|
|
94
|
+
let envelope = 1;
|
|
95
|
+
let fadeStarted = 0;
|
|
96
|
+
let playing = false;
|
|
97
|
+
let fadeTimer;
|
|
98
|
+
let stopping;
|
|
99
|
+
let amplifier;
|
|
100
|
+
const pendingGain = /* @__PURE__ */ new Set();
|
|
101
|
+
let disposed = false;
|
|
102
|
+
let generation = 0;
|
|
103
|
+
let ended = Promise.resolve();
|
|
104
|
+
let settle;
|
|
105
|
+
let rejectStart;
|
|
106
|
+
let detachAbort;
|
|
107
|
+
const pendingMetadata = /* @__PURE__ */ new Set();
|
|
108
|
+
const requireActive = () => {
|
|
109
|
+
if (disposed) throw new Error("Audio playback is disposed.");
|
|
110
|
+
};
|
|
111
|
+
const clearFadeTimer = () => {
|
|
112
|
+
if (fadeTimer !== void 0) clearTimeout(fadeTimer);
|
|
113
|
+
fadeTimer = void 0;
|
|
114
|
+
};
|
|
115
|
+
const finish = (error) => {
|
|
116
|
+
playing = false;
|
|
117
|
+
for (const reject of pendingGain) reject(error ?? aborted());
|
|
118
|
+
pendingGain.clear();
|
|
119
|
+
clearFadeTimer();
|
|
120
|
+
const previousStop = stopping;
|
|
121
|
+
stopping = void 0;
|
|
122
|
+
previousStop?.detach();
|
|
123
|
+
previousStop?.reject(error ?? aborted());
|
|
124
|
+
generation++;
|
|
125
|
+
rejectStart?.(error ?? aborted());
|
|
126
|
+
rejectStart = void 0;
|
|
127
|
+
detachAbort?.();
|
|
128
|
+
detachAbort = void 0;
|
|
129
|
+
settle?.(error);
|
|
130
|
+
settle = void 0;
|
|
131
|
+
};
|
|
132
|
+
const writeVolume = () => {
|
|
133
|
+
audio.volume = Math.min(1, gain) * envelope;
|
|
134
|
+
amplifier?.setGain(gain);
|
|
135
|
+
};
|
|
136
|
+
const prepareAmplifier = () => {
|
|
137
|
+
if (!amplifier && gain > 1) amplifier = createAudioAmplifier(audio);
|
|
138
|
+
writeVolume();
|
|
139
|
+
};
|
|
140
|
+
const stopNow = () => {
|
|
141
|
+
const completed = stopping;
|
|
142
|
+
stopping = void 0;
|
|
143
|
+
completed?.detach();
|
|
144
|
+
audio.pause();
|
|
145
|
+
audio.loop = false;
|
|
146
|
+
audio.currentTime = 0;
|
|
147
|
+
finish();
|
|
148
|
+
envelope = 1;
|
|
149
|
+
writeVolume();
|
|
150
|
+
completed?.resolve();
|
|
151
|
+
};
|
|
152
|
+
const refreshFade = () => {
|
|
153
|
+
clearFadeTimer();
|
|
154
|
+
if (disposed || !playing && !stopping) return;
|
|
155
|
+
const now = performance.now();
|
|
156
|
+
let delay = 16;
|
|
157
|
+
if (stopping) {
|
|
158
|
+
const progress = Math.min(1, (now - stopping.started) / (stopping.seconds * 1e3));
|
|
159
|
+
envelope = stopping.from * (1 - progress);
|
|
160
|
+
writeVolume();
|
|
161
|
+
if (progress >= 1) {
|
|
162
|
+
stopNow();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
const fadeIn = activeFadeIn === 0 ? 1 : Math.min(1, (now - fadeStarted) / (activeFadeIn * 1e3));
|
|
167
|
+
const remaining = Number.isFinite(audio.duration) ? Math.max(0, audio.duration - audio.currentTime) / audio.playbackRate : Infinity;
|
|
168
|
+
const fadeOut = !audio.loop && activeFadeOut > 0 ? Math.min(1, remaining / activeFadeOut) : 1;
|
|
169
|
+
envelope = Math.max(0, Math.min(fadeIn, fadeOut));
|
|
170
|
+
writeVolume();
|
|
171
|
+
if (fadeIn >= 1) {
|
|
172
|
+
if (audio.loop || activeFadeOut === 0) return;
|
|
173
|
+
if (!Number.isFinite(remaining)) delay = 250;
|
|
174
|
+
else if (remaining > activeFadeOut) delay = Math.min(250, Math.max(16, (remaining - activeFadeOut) * 1e3));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
fadeTimer = setTimeout(refreshFade, delay);
|
|
178
|
+
};
|
|
179
|
+
const onEnded = () => {
|
|
180
|
+
if (stopping) {
|
|
181
|
+
stopNow();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
finish();
|
|
185
|
+
if (activeFadeOut > 0 && !audio.loop) {
|
|
186
|
+
envelope = 0;
|
|
187
|
+
writeVolume();
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
const onError = () => {
|
|
191
|
+
audio.pause();
|
|
192
|
+
const error = playbackError(audio.error ?? /* @__PURE__ */ new Error("The browser audio element reported a playback error."));
|
|
193
|
+
finish(error);
|
|
194
|
+
for (const complete of pendingMetadata) complete(error);
|
|
195
|
+
};
|
|
196
|
+
audio.addEventListener("ended", onEnded);
|
|
197
|
+
audio.addEventListener("error", onError);
|
|
198
|
+
for (const event of [
|
|
199
|
+
"durationchange",
|
|
200
|
+
"ratechange",
|
|
201
|
+
"seeked"
|
|
202
|
+
]) audio.addEventListener(event, refreshFade);
|
|
203
|
+
return Object.freeze({
|
|
204
|
+
get ended() {
|
|
205
|
+
return ended;
|
|
206
|
+
},
|
|
207
|
+
get currentTime() {
|
|
208
|
+
return audio.currentTime;
|
|
209
|
+
},
|
|
210
|
+
get paused() {
|
|
211
|
+
return audio.paused;
|
|
212
|
+
},
|
|
213
|
+
get loop() {
|
|
214
|
+
return audio.loop;
|
|
215
|
+
},
|
|
216
|
+
get gain() {
|
|
217
|
+
return gain;
|
|
218
|
+
},
|
|
219
|
+
get muted() {
|
|
220
|
+
return audio.muted;
|
|
221
|
+
},
|
|
222
|
+
get playbackRate() {
|
|
223
|
+
return audio.playbackRate;
|
|
224
|
+
},
|
|
225
|
+
async play(playOptions = {}) {
|
|
226
|
+
requireActive();
|
|
227
|
+
if (!playOptions || typeof playOptions !== "object" || Array.isArray(playOptions) || Object.keys(playOptions).some((key) => ![
|
|
228
|
+
"restart",
|
|
229
|
+
"loop",
|
|
230
|
+
"signal",
|
|
231
|
+
"fadeIn",
|
|
232
|
+
"fadeOut",
|
|
233
|
+
"gain"
|
|
234
|
+
].includes(key)) || playOptions.restart !== void 0 && typeof playOptions.restart !== "boolean" || playOptions.loop !== void 0 && typeof playOptions.loop !== "boolean") throw new TypeError("Invalid audio play options.");
|
|
235
|
+
const fadeIn = fadeSeconds(playOptions.fadeIn === void 0 ? defaultFadeIn : playOptions.fadeIn, "fadeIn");
|
|
236
|
+
const fadeOut = fadeSeconds(playOptions.fadeOut === void 0 ? defaultFadeOut : playOptions.fadeOut, "fadeOut");
|
|
237
|
+
const nextGain = playOptions.gain === void 0 ? gain : playbackGain(playOptions.gain);
|
|
238
|
+
const signal = playOptions.signal;
|
|
239
|
+
if (signal?.aborted) throw aborted();
|
|
240
|
+
finish();
|
|
241
|
+
const run = generation;
|
|
242
|
+
gain = nextGain;
|
|
243
|
+
if (playOptions.restart) audio.currentTime = 0;
|
|
244
|
+
audio.loop = playOptions.loop ?? audio.loop;
|
|
245
|
+
activeFadeIn = fadeIn;
|
|
246
|
+
activeFadeOut = fadeOut;
|
|
247
|
+
envelope = fadeIn > 0 ? 0 : 1;
|
|
248
|
+
writeVolume();
|
|
249
|
+
ended = new Promise((resolve, reject) => {
|
|
250
|
+
settle = (error) => error === void 0 ? resolve() : reject(error);
|
|
251
|
+
});
|
|
252
|
+
ended.catch(() => {});
|
|
253
|
+
const onAbort = () => {
|
|
254
|
+
audio.pause();
|
|
255
|
+
finish(aborted());
|
|
256
|
+
};
|
|
257
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
258
|
+
detachAbort = () => signal?.removeEventListener("abort", onAbort);
|
|
259
|
+
try {
|
|
260
|
+
prepareAmplifier();
|
|
261
|
+
const started = audio.play();
|
|
262
|
+
const ready = amplifier ? Promise.all([started, amplifier.resume()]) : Promise.resolve(started);
|
|
263
|
+
await Promise.race([ready, new Promise((_resolve, reject) => {
|
|
264
|
+
rejectStart = reject;
|
|
265
|
+
})]);
|
|
266
|
+
if (run !== generation) throw aborted();
|
|
267
|
+
rejectStart = void 0;
|
|
268
|
+
playing = true;
|
|
269
|
+
fadeStarted = performance.now();
|
|
270
|
+
refreshFade();
|
|
271
|
+
} catch (cause) {
|
|
272
|
+
if (run !== generation) throw cause;
|
|
273
|
+
audio.pause();
|
|
274
|
+
const error = playbackError(cause);
|
|
275
|
+
finish(error);
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
pause() {
|
|
280
|
+
requireActive();
|
|
281
|
+
audio.pause();
|
|
282
|
+
finish();
|
|
283
|
+
},
|
|
284
|
+
stop(stopOptions = {}) {
|
|
285
|
+
requireActive();
|
|
286
|
+
if (!stopOptions || typeof stopOptions !== "object" || Array.isArray(stopOptions) || Object.keys(stopOptions).some((key) => !["fadeOut", "signal"].includes(key))) throw new TypeError("Invalid audio stop options.");
|
|
287
|
+
const seconds = fadeSeconds(stopOptions.fadeOut === void 0 ? activeFadeOut : stopOptions.fadeOut, "fadeOut");
|
|
288
|
+
const signal = stopOptions.signal;
|
|
289
|
+
if (signal?.aborted) throw aborted();
|
|
290
|
+
if (seconds === 0 || audio.paused || !playing) {
|
|
291
|
+
stopNow();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const previous = stopping;
|
|
295
|
+
stopping = void 0;
|
|
296
|
+
previous?.detach();
|
|
297
|
+
previous?.reject(aborted());
|
|
298
|
+
return new Promise((resolve, reject) => {
|
|
299
|
+
const onAbort = () => {
|
|
300
|
+
const current = stopping;
|
|
301
|
+
stopping = void 0;
|
|
302
|
+
current?.detach();
|
|
303
|
+
stopNow();
|
|
304
|
+
reject(aborted());
|
|
305
|
+
};
|
|
306
|
+
stopping = {
|
|
307
|
+
started: performance.now(),
|
|
308
|
+
seconds,
|
|
309
|
+
from: envelope,
|
|
310
|
+
resolve,
|
|
311
|
+
reject,
|
|
312
|
+
detach: () => signal?.removeEventListener("abort", onAbort)
|
|
313
|
+
};
|
|
314
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
315
|
+
if (signal?.aborted) onAbort();
|
|
316
|
+
else refreshFade();
|
|
317
|
+
});
|
|
318
|
+
},
|
|
319
|
+
async seek(seconds, signal) {
|
|
320
|
+
requireActive();
|
|
321
|
+
if (!Number.isFinite(seconds) || seconds < 0) throw new TypeError("Audio seek time must be non-negative.");
|
|
322
|
+
if (signal?.aborted) throw aborted();
|
|
323
|
+
if (audio.readyState < 1) await new Promise((resolve, reject) => {
|
|
324
|
+
const complete = (error) => {
|
|
325
|
+
clearTimeout(timer);
|
|
326
|
+
audio.removeEventListener("loadedmetadata", ready);
|
|
327
|
+
signal?.removeEventListener("abort", cancel);
|
|
328
|
+
pendingMetadata.delete(complete);
|
|
329
|
+
if (error === void 0) resolve();
|
|
330
|
+
else reject(error);
|
|
331
|
+
};
|
|
332
|
+
const ready = () => complete();
|
|
333
|
+
const cancel = () => complete(aborted());
|
|
334
|
+
const timer = setTimeout(() => complete(/* @__PURE__ */ new Error("Audio metadata did not become ready.")), 1e4);
|
|
335
|
+
pendingMetadata.add(complete);
|
|
336
|
+
audio.addEventListener("loadedmetadata", ready, { once: true });
|
|
337
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
338
|
+
if (signal?.aborted) cancel();
|
|
339
|
+
else if (audio.readyState >= 1) ready();
|
|
340
|
+
});
|
|
341
|
+
requireActive();
|
|
342
|
+
if (signal?.aborted) throw aborted();
|
|
343
|
+
if (Number.isFinite(audio.duration) && seconds > audio.duration) throw new RangeError("Audio seek exceeds its duration.");
|
|
344
|
+
audio.currentTime = seconds;
|
|
345
|
+
refreshFade();
|
|
346
|
+
},
|
|
347
|
+
setGain(value) {
|
|
348
|
+
const normalized = playbackGain(value);
|
|
349
|
+
requireActive();
|
|
350
|
+
const previous = gain;
|
|
351
|
+
gain = normalized;
|
|
352
|
+
try {
|
|
353
|
+
if (playing) prepareAmplifier();
|
|
354
|
+
else writeVolume();
|
|
355
|
+
} catch (error) {
|
|
356
|
+
gain = previous;
|
|
357
|
+
writeVolume();
|
|
358
|
+
throw playbackError(error);
|
|
359
|
+
}
|
|
360
|
+
if (playing && amplifier) {
|
|
361
|
+
const run = generation;
|
|
362
|
+
let rejectResume;
|
|
363
|
+
const cancelled = new Promise((_resolve, reject) => {
|
|
364
|
+
rejectResume = reject;
|
|
365
|
+
});
|
|
366
|
+
pendingGain.add(rejectResume);
|
|
367
|
+
return Promise.race([amplifier.resume(), cancelled]).catch((cause) => {
|
|
368
|
+
if (run !== generation) throw cause;
|
|
369
|
+
audio.pause();
|
|
370
|
+
const error = playbackError(cause);
|
|
371
|
+
finish(error);
|
|
372
|
+
throw error;
|
|
373
|
+
}).finally(() => pendingGain.delete(rejectResume));
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
setMuted(value) {
|
|
377
|
+
if (typeof value !== "boolean") throw new TypeError("Audio muted must be a boolean.");
|
|
378
|
+
requireActive();
|
|
379
|
+
audio.muted = value;
|
|
380
|
+
},
|
|
381
|
+
setPlaybackRate(value) {
|
|
382
|
+
const normalized = rate(value);
|
|
383
|
+
requireActive();
|
|
384
|
+
audio.playbackRate = normalized;
|
|
385
|
+
refreshFade();
|
|
386
|
+
},
|
|
387
|
+
dispose() {
|
|
388
|
+
if (disposed) return;
|
|
389
|
+
disposed = true;
|
|
390
|
+
try {
|
|
391
|
+
audio.pause();
|
|
392
|
+
audio.currentTime = 0;
|
|
393
|
+
} catch {}
|
|
394
|
+
finish();
|
|
395
|
+
for (const complete of pendingMetadata) complete(aborted());
|
|
396
|
+
audio.removeEventListener("ended", onEnded);
|
|
397
|
+
audio.removeEventListener("error", onError);
|
|
398
|
+
for (const event of [
|
|
399
|
+
"durationchange",
|
|
400
|
+
"ratechange",
|
|
401
|
+
"seeked"
|
|
402
|
+
]) audio.removeEventListener(event, refreshFade);
|
|
403
|
+
try {
|
|
404
|
+
audio.removeAttribute?.("src");
|
|
405
|
+
audio.load?.();
|
|
406
|
+
} finally {
|
|
407
|
+
try {
|
|
408
|
+
amplifier?.dispose();
|
|
409
|
+
} finally {
|
|
410
|
+
release();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
1
416
|
var block_definitions_default = {
|
|
2
417
|
extensionName: "Asset Cache",
|
|
3
418
|
blocks: [
|
|
@@ -278,7 +693,404 @@ var block_definitions_default = {
|
|
|
278
693
|
]
|
|
279
694
|
};
|
|
280
695
|
//#endregion
|
|
281
|
-
//#region node_modules/.pnpm/@kubohiroya+turbowarp-
|
|
696
|
+
//#region node_modules/.pnpm/@kubohiroya+turbowarp-extension-data-editor@0.2.1_react@18.3.1/node_modules/@kubohiroya/turbowarp-extension-data-editor/dist/fixed-input-schema.js
|
|
697
|
+
var numberToken = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/u;
|
|
698
|
+
var invalidNumber = Object.freeze({
|
|
699
|
+
kind: "invalid",
|
|
700
|
+
code: "fixed-number-input-invalid"
|
|
701
|
+
});
|
|
702
|
+
/**
|
|
703
|
+
* Parse a NEW numeric draft according to NP-finite-new-input-v1.
|
|
704
|
+
* Never apply this to opening/no-op raw values or use it as an owner validator.
|
|
705
|
+
* Keep the validated token so negative zero, precision and exponent spelling survive.
|
|
706
|
+
*/
|
|
707
|
+
function parseFixedNumberInput(input) {
|
|
708
|
+
if (typeof input === "number") {
|
|
709
|
+
if (!Number.isFinite(input)) return invalidNumber;
|
|
710
|
+
return Object.freeze({
|
|
711
|
+
kind: "valid",
|
|
712
|
+
token: Object.is(input, -0) ? "-0" : String(input),
|
|
713
|
+
value: input
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
if (typeof input !== "string") return invalidNumber;
|
|
717
|
+
const token = input.trim();
|
|
718
|
+
if (!numberToken.test(token)) return invalidNumber;
|
|
719
|
+
const value = Number(token);
|
|
720
|
+
return Number.isFinite(value) ? Object.freeze({
|
|
721
|
+
kind: "valid",
|
|
722
|
+
token,
|
|
723
|
+
value
|
|
724
|
+
}) : invalidNumber;
|
|
725
|
+
}
|
|
726
|
+
var block_editor_fixed_inputs_schema_default = {
|
|
727
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
728
|
+
$id: "https://kubohiroya.github.io/turbowarp-asset-cache/schemas/block-editor-fixed-inputs.schema.json",
|
|
729
|
+
$defs: {
|
|
730
|
+
"setTextStyle": {
|
|
731
|
+
"type": "object",
|
|
732
|
+
"properties": {
|
|
733
|
+
"PROPERTY": {
|
|
734
|
+
"type": "string",
|
|
735
|
+
"enum": [
|
|
736
|
+
"animation",
|
|
737
|
+
"font",
|
|
738
|
+
"color",
|
|
739
|
+
"width",
|
|
740
|
+
"align"
|
|
741
|
+
]
|
|
742
|
+
},
|
|
743
|
+
"VALUE": { "type": "string" }
|
|
744
|
+
},
|
|
745
|
+
"additionalProperties": false
|
|
746
|
+
},
|
|
747
|
+
"animation": {
|
|
748
|
+
"type": "string",
|
|
749
|
+
"enum": [
|
|
750
|
+
"",
|
|
751
|
+
"none",
|
|
752
|
+
"type",
|
|
753
|
+
"rainbow",
|
|
754
|
+
"zoom",
|
|
755
|
+
"shake"
|
|
756
|
+
]
|
|
757
|
+
},
|
|
758
|
+
"font": { "type": "string" },
|
|
759
|
+
"color": {
|
|
760
|
+
"type": "string",
|
|
761
|
+
"pattern": "^(?:|#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6})$"
|
|
762
|
+
},
|
|
763
|
+
"width": {
|
|
764
|
+
"type": "string",
|
|
765
|
+
"$comment": "Empty/trimmed empty resets; otherwise normalizeTextStyleValue requires Number(value) finite and positive. Preserve the existing STRING grammar, including hexadecimal and leading plus."
|
|
766
|
+
},
|
|
767
|
+
"align": {
|
|
768
|
+
"type": "string",
|
|
769
|
+
"enum": [
|
|
770
|
+
"",
|
|
771
|
+
"left",
|
|
772
|
+
"center",
|
|
773
|
+
"right"
|
|
774
|
+
]
|
|
775
|
+
},
|
|
776
|
+
"loadingCostumeAt": {
|
|
777
|
+
"type": "object",
|
|
778
|
+
"properties": { "INDEX": { "type": "number" } },
|
|
779
|
+
"additionalProperties": false
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
//#endregion
|
|
784
|
+
//#region src/fixed-input-schema.ts
|
|
785
|
+
/** Canonical owner schema fragments; no network resources or runtime providers. */
|
|
786
|
+
function fixedInputBundle(fragment, schema) {
|
|
787
|
+
const source = {
|
|
788
|
+
repository: "turbowarp-asset-cache",
|
|
789
|
+
path: "schemas/block-editor-fixed-inputs.schema.json",
|
|
790
|
+
symbol: {
|
|
791
|
+
"setTextStyle-PROPERTY": "#/$defs/setTextStyle/properties/PROPERTY",
|
|
792
|
+
"setTextStyle-VALUE": "#/$defs/setTextStyle/properties/VALUE",
|
|
793
|
+
"loadingCostumeAt-INDEX": "#/$defs/loadingCostumeAt/properties/INDEX"
|
|
794
|
+
}[fragment] ?? `#/$defs/${fragment}`
|
|
795
|
+
};
|
|
796
|
+
return {
|
|
797
|
+
identity: {
|
|
798
|
+
ownerId: "turbowarp-asset-cache",
|
|
799
|
+
contractId: "fixed-inputs",
|
|
800
|
+
revision: "1",
|
|
801
|
+
variantId: fragment
|
|
802
|
+
},
|
|
803
|
+
dialect: "2020-12",
|
|
804
|
+
root: {
|
|
805
|
+
retrievalUri: block_editor_fixed_inputs_schema_default.$id,
|
|
806
|
+
document: schema,
|
|
807
|
+
artifact: {
|
|
808
|
+
role: "canonical-schema",
|
|
809
|
+
source
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
resources: [],
|
|
813
|
+
provenance: {
|
|
814
|
+
canonicalSources: [
|
|
815
|
+
source,
|
|
816
|
+
{
|
|
817
|
+
repository: "turbowarp-asset-cache",
|
|
818
|
+
path: "src/text-style.ts",
|
|
819
|
+
symbol: "normalizeTextStyleProperty / normalizeTextStyleValue"
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
repository: "turbowarp-asset-cache",
|
|
823
|
+
path: "src/extension.ts",
|
|
824
|
+
symbol: "getInfo / loadingCostumeAt"
|
|
825
|
+
}
|
|
826
|
+
],
|
|
827
|
+
generatedFiles: [],
|
|
828
|
+
referenceSchemas: [],
|
|
829
|
+
generationSources: []
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
var fixedSchemas = block_editor_fixed_inputs_schema_default.$defs;
|
|
834
|
+
//#endregion
|
|
835
|
+
//#region src/text-style.ts
|
|
836
|
+
var TEXT_RUNTIME_NAMESPACE = "text";
|
|
837
|
+
var TEXT_STYLE_RUNTIME_NAMESPACE = "textStyle";
|
|
838
|
+
var TEXT_STYLE_PROPERTIES = [
|
|
839
|
+
"animation",
|
|
840
|
+
"font",
|
|
841
|
+
"color",
|
|
842
|
+
"width",
|
|
843
|
+
"align"
|
|
844
|
+
];
|
|
845
|
+
var DEFAULT_STAGE_WIDTH = 480;
|
|
846
|
+
var DEFAULT_FONT = "Handwriting";
|
|
847
|
+
var DEFAULT_COLOR = "#ffffff";
|
|
848
|
+
var DEFAULT_ALIGNMENT = "center";
|
|
849
|
+
var DEFAULT_OUTLINE_COLOR = "#000000";
|
|
850
|
+
function textRuntimeVariableName(name) {
|
|
851
|
+
return `${TEXT_RUNTIME_NAMESPACE}:${name}`;
|
|
852
|
+
}
|
|
853
|
+
function textStyleRuntimeVariableName(name, property) {
|
|
854
|
+
return `${TEXT_STYLE_RUNTIME_NAMESPACE}:${name}:${property}`;
|
|
855
|
+
}
|
|
856
|
+
function normalizeTextStyleProperty(value) {
|
|
857
|
+
const property = String(value ?? "").trim().toLowerCase();
|
|
858
|
+
if (TEXT_STYLE_PROPERTIES.includes(property)) return property;
|
|
859
|
+
throw new Error(`Unknown text style property: ${property || "(empty)"}`);
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Normalize a DSL-facing style value for runtime-variable storage.
|
|
863
|
+
* An empty value intentionally resets the property to its default.
|
|
864
|
+
*/
|
|
865
|
+
function normalizeTextStyleValue(property, value) {
|
|
866
|
+
const raw = String(value ?? "").trim();
|
|
867
|
+
if (!raw) return "";
|
|
868
|
+
switch (property) {
|
|
869
|
+
case "animation": {
|
|
870
|
+
const animation = raw.toLowerCase() === "typing" ? "type" : raw.toLowerCase();
|
|
871
|
+
if (animation === "none" || animation === "type" || animation === "rainbow" || animation === "zoom" || animation === "shake") return animation;
|
|
872
|
+
throw new Error(`Invalid text animation: ${raw}`);
|
|
873
|
+
}
|
|
874
|
+
case "font": return raw;
|
|
875
|
+
case "color": {
|
|
876
|
+
if (/^#[0-9a-f]{6}$/i.test(raw)) return raw.toLowerCase();
|
|
877
|
+
const shortColor = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(raw);
|
|
878
|
+
if (shortColor) return `#${shortColor[1]}${shortColor[1]}${shortColor[2]}${shortColor[2]}${shortColor[3]}${shortColor[3]}`.toLowerCase();
|
|
879
|
+
throw new Error(`Invalid text color: ${raw}`);
|
|
880
|
+
}
|
|
881
|
+
case "width": {
|
|
882
|
+
const width = Number(raw);
|
|
883
|
+
if (!Number.isFinite(width) || width <= 0) throw new Error(`Text width must be a positive number: ${raw}`);
|
|
884
|
+
return String(width);
|
|
885
|
+
}
|
|
886
|
+
case "align": {
|
|
887
|
+
const align = raw.toLowerCase();
|
|
888
|
+
if (align === "left" || align === "center" || align === "right") return align;
|
|
889
|
+
throw new Error(`Invalid text alignment: ${raw}`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
function resolveTextStyle(name, stageWidth, getRuntimeVariable) {
|
|
894
|
+
const width = Number(stageWidth);
|
|
895
|
+
const defaults = {
|
|
896
|
+
animation: "none",
|
|
897
|
+
font: DEFAULT_FONT,
|
|
898
|
+
color: DEFAULT_COLOR,
|
|
899
|
+
width: Number.isFinite(width) && width > 0 ? width : DEFAULT_STAGE_WIDTH,
|
|
900
|
+
align: DEFAULT_ALIGNMENT
|
|
901
|
+
};
|
|
902
|
+
const read = (property) => normalizeTextStyleValue(property, getRuntimeVariable(textStyleRuntimeVariableName(name, property)));
|
|
903
|
+
const animation = read("animation");
|
|
904
|
+
const font = read("font");
|
|
905
|
+
const color = read("color");
|
|
906
|
+
const configuredWidth = read("width");
|
|
907
|
+
const align = read("align");
|
|
908
|
+
return {
|
|
909
|
+
animation: animation ? animation : defaults.animation,
|
|
910
|
+
font: font || defaults.font,
|
|
911
|
+
color: color || defaults.color,
|
|
912
|
+
width: configuredWidth ? Number(configuredWidth) : defaults.width,
|
|
913
|
+
align: align ? align : defaults.align
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
//#endregion
|
|
917
|
+
//#region src/fixed-input-editors.ts
|
|
918
|
+
var extensionId = "kubohiroyaassetcache";
|
|
919
|
+
var choices = (values) => values.map((value) => ({ value }));
|
|
920
|
+
var context = () => ({
|
|
921
|
+
consumerId: "turbowarp-asset-cache",
|
|
922
|
+
nowMs: 0,
|
|
923
|
+
settings: null
|
|
924
|
+
});
|
|
925
|
+
var string = (value) => {
|
|
926
|
+
if (typeof value !== "string") throw new TypeError("Expected a string literal.");
|
|
927
|
+
return value;
|
|
928
|
+
};
|
|
929
|
+
var issue = () => [{ message: "Invalid asset style value." }];
|
|
930
|
+
/** New edits preserve the candidate token; existing aliases are display projections only. */
|
|
931
|
+
function styleBinding(property) {
|
|
932
|
+
const values = property === "animation" ? [
|
|
933
|
+
"",
|
|
934
|
+
"none",
|
|
935
|
+
"type",
|
|
936
|
+
"rainbow",
|
|
937
|
+
"zoom",
|
|
938
|
+
"shake"
|
|
939
|
+
] : property === "align" ? [
|
|
940
|
+
"",
|
|
941
|
+
"left",
|
|
942
|
+
"center",
|
|
943
|
+
"right"
|
|
944
|
+
] : void 0;
|
|
945
|
+
return {
|
|
946
|
+
argument: "VALUE",
|
|
947
|
+
schemaBundle: fixedInputBundle(property, fixedSchemas[property]),
|
|
948
|
+
control: {
|
|
949
|
+
kind: values ? "enum" : property === "color" ? "color" : property === "width" ? "number" : "text",
|
|
950
|
+
reset: "",
|
|
951
|
+
...values ? { choices: choices(values) } : {}
|
|
952
|
+
},
|
|
953
|
+
read(value) {
|
|
954
|
+
return normalizeTextStyleValue(property, string(value));
|
|
955
|
+
},
|
|
956
|
+
encode(value) {
|
|
957
|
+
const raw = string(value);
|
|
958
|
+
if (property !== "width" || raw === "") return raw;
|
|
959
|
+
normalizeTextStyleValue(property, raw);
|
|
960
|
+
return raw;
|
|
961
|
+
},
|
|
962
|
+
validateCandidate(value) {
|
|
963
|
+
try {
|
|
964
|
+
normalizeTextStyleValue(property, string(value));
|
|
965
|
+
return [];
|
|
966
|
+
} catch {
|
|
967
|
+
return issue();
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
var fixedInputEditors = [{
|
|
973
|
+
apiVersion: 1,
|
|
974
|
+
extensionId,
|
|
975
|
+
editorId: "text-style-fixed",
|
|
976
|
+
opcodes: [`${extensionId}_setTextStyle`],
|
|
977
|
+
contractId: "CP-asset-cache",
|
|
978
|
+
contractRevision: "1",
|
|
979
|
+
validationContext: context,
|
|
980
|
+
bindings: [{
|
|
981
|
+
argument: "PROPERTY",
|
|
982
|
+
schemaBundle: fixedInputBundle("setTextStyle-PROPERTY", fixedSchemas.setTextStyle.properties.PROPERTY),
|
|
983
|
+
control: {
|
|
984
|
+
kind: "enum",
|
|
985
|
+
choices: choices(TEXT_STYLE_PROPERTIES)
|
|
986
|
+
},
|
|
987
|
+
read(value) {
|
|
988
|
+
const raw = string(value);
|
|
989
|
+
try {
|
|
990
|
+
return normalizeTextStyleProperty(raw);
|
|
991
|
+
} catch {
|
|
992
|
+
return raw;
|
|
993
|
+
}
|
|
994
|
+
},
|
|
995
|
+
encode: (value) => string(value),
|
|
996
|
+
validateCandidate(value) {
|
|
997
|
+
try {
|
|
998
|
+
normalizeTextStyleProperty(value);
|
|
999
|
+
return [];
|
|
1000
|
+
} catch {
|
|
1001
|
+
return issue();
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}, {
|
|
1005
|
+
argument: "VALUE",
|
|
1006
|
+
schemaBundle: fixedInputBundle("setTextStyle-VALUE", fixedSchemas.setTextStyle.properties.VALUE),
|
|
1007
|
+
read: (value) => string(value),
|
|
1008
|
+
encode: (value) => string(value),
|
|
1009
|
+
validateCandidate: () => []
|
|
1010
|
+
}],
|
|
1011
|
+
dependencies: [{
|
|
1012
|
+
selector: "PROPERTY",
|
|
1013
|
+
dependents: ["VALUE"]
|
|
1014
|
+
}],
|
|
1015
|
+
branchPolicies: [{
|
|
1016
|
+
selector: "PROPERTY",
|
|
1017
|
+
normalize: {
|
|
1018
|
+
trim: true,
|
|
1019
|
+
lowercase: true
|
|
1020
|
+
},
|
|
1021
|
+
cases: TEXT_STYLE_PROPERTIES.map((property) => ({
|
|
1022
|
+
values: [property],
|
|
1023
|
+
activeDependents: ["VALUE"],
|
|
1024
|
+
bindings: [styleBinding(property)]
|
|
1025
|
+
}))
|
|
1026
|
+
}]
|
|
1027
|
+
}, {
|
|
1028
|
+
apiVersion: 1,
|
|
1029
|
+
extensionId,
|
|
1030
|
+
editorId: "loading-costume-index-fixed",
|
|
1031
|
+
opcodes: [`${extensionId}_loadingCostumeAt`],
|
|
1032
|
+
contractId: "turbowarp-asset-cache/loadingCostumeAt.INDEX",
|
|
1033
|
+
contractRevision: "1",
|
|
1034
|
+
validationContext: context,
|
|
1035
|
+
bindings: [{
|
|
1036
|
+
argument: "INDEX",
|
|
1037
|
+
schemaBundle: fixedInputBundle("loadingCostumeAt-INDEX", fixedSchemas.loadingCostumeAt.properties.INDEX),
|
|
1038
|
+
control: { kind: "number" },
|
|
1039
|
+
read(value) {
|
|
1040
|
+
if (typeof value !== "number" && typeof value !== "string" || typeof value === "string" && value.trim() === "") throw new TypeError("Non-numeric index.");
|
|
1041
|
+
const number = Number(value);
|
|
1042
|
+
if (!Number.isFinite(number)) throw new TypeError("Nonfinite index.");
|
|
1043
|
+
return number;
|
|
1044
|
+
},
|
|
1045
|
+
encode(value) {
|
|
1046
|
+
const parsed = parseFixedNumberInput(value);
|
|
1047
|
+
if (parsed.kind !== "valid") throw new TypeError("Invalid index.");
|
|
1048
|
+
return parsed.token;
|
|
1049
|
+
},
|
|
1050
|
+
validateCandidate(value) {
|
|
1051
|
+
return Number.isFinite(Number(value)) ? [] : [{ message: "Invalid index." }];
|
|
1052
|
+
}
|
|
1053
|
+
}]
|
|
1054
|
+
}];
|
|
1055
|
+
/** Per-instance, idempotent registration. Legacy hosts never acquire the fixed capability. */
|
|
1056
|
+
function fixedInputRegistration(scratch) {
|
|
1057
|
+
const host = scratch;
|
|
1058
|
+
let active;
|
|
1059
|
+
let removers = [];
|
|
1060
|
+
let disposed = false;
|
|
1061
|
+
const dispose = () => {
|
|
1062
|
+
host.vm?.runtime?.off?.("RUNTIME_DISPOSED", shutdown);
|
|
1063
|
+
for (const remove of removers.splice(0).reverse()) remove();
|
|
1064
|
+
active = void 0;
|
|
1065
|
+
};
|
|
1066
|
+
const shutdown = () => {
|
|
1067
|
+
disposed = true;
|
|
1068
|
+
dispose();
|
|
1069
|
+
};
|
|
1070
|
+
return {
|
|
1071
|
+
ensure(opcodes) {
|
|
1072
|
+
const api = host.gui?.blockEditors;
|
|
1073
|
+
if (disposed || api === active) return;
|
|
1074
|
+
dispose();
|
|
1075
|
+
if (api?.apiVersion !== 1 || typeof api.registerFixedInputEditor !== "function") return;
|
|
1076
|
+
try {
|
|
1077
|
+
for (const definition of fixedInputEditors) if (definition.opcodes.every((opcode) => opcodes.includes(opcode))) {
|
|
1078
|
+
const remove = api.registerFixedInputEditor.call(api, definition);
|
|
1079
|
+
if (typeof remove !== "function") throw new TypeError("Invalid fixed registration cleanup.");
|
|
1080
|
+
removers.push(remove);
|
|
1081
|
+
}
|
|
1082
|
+
active = api;
|
|
1083
|
+
host.vm?.runtime?.on?.("RUNTIME_DISPOSED", shutdown);
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
dispose();
|
|
1086
|
+
throw error;
|
|
1087
|
+
}
|
|
1088
|
+
},
|
|
1089
|
+
dispose: shutdown
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
//#endregion
|
|
1093
|
+
//#region node_modules/.pnpm/@kubohiroya+turbowarp-named-data@0.3.1/node_modules/@kubohiroya/turbowarp-named-data/dist/composition.js
|
|
282
1094
|
var NAMED_DATA_REGISTRY_SYMBOL_KEY = "@kubohiroya/turbowarp-named-data/registry";
|
|
283
1095
|
var NAMED_DATA_REGISTRY_SYMBOL = Symbol.for(NAMED_DATA_REGISTRY_SYMBOL_KEY);
|
|
284
1096
|
var NAMED_DATA_NAMESPACE_PATTERN = /^[a-z][a-z0-9.-]{0,63}$/u;
|
|
@@ -299,6 +1111,8 @@ var NAMED_DATA_REPRESENTATIONS = [
|
|
|
299
1111
|
"markdown",
|
|
300
1112
|
"raw"
|
|
301
1113
|
];
|
|
1114
|
+
var NAMED_DATA_SCHEMA_ID_PATTERN = /^[a-z][a-z0-9.-]{0,127}$/u;
|
|
1115
|
+
var NAMED_DATA_SCHEMA_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/u;
|
|
302
1116
|
var NAMED_DATA_ERROR_CODES = [
|
|
303
1117
|
"NAMED_DATA_INVALID_REF",
|
|
304
1118
|
"NAMED_DATA_INVALID_REGISTRY",
|
|
@@ -307,6 +1121,9 @@ var NAMED_DATA_ERROR_CODES = [
|
|
|
307
1121
|
"NAMED_DATA_NOT_FOUND",
|
|
308
1122
|
"NAMED_DATA_KIND_MISMATCH",
|
|
309
1123
|
"NAMED_DATA_SCOPE_MISMATCH",
|
|
1124
|
+
"NAMED_DATA_SCHEMA_NOT_FOUND",
|
|
1125
|
+
"NAMED_DATA_SCHEMA_VERSION_MISMATCH",
|
|
1126
|
+
"NAMED_DATA_SCHEMA_UNRESOLVED",
|
|
310
1127
|
"NAMED_DATA_REPRESENTATION_UNSUPPORTED",
|
|
311
1128
|
"NAMED_DATA_INVALID_METADATA",
|
|
312
1129
|
"NAMED_DATA_BODY_TOO_LARGE",
|
|
@@ -527,9 +1344,21 @@ function validateReferenceShape(reference, representation) {
|
|
|
527
1344
|
if (!reference || typeof reference !== "object") throw invalidReference$1();
|
|
528
1345
|
requireNamespace(reference.namespace);
|
|
529
1346
|
if (typeof reference.name !== "string" || reference.name.length === 0 || reference.name.length > 256 || containsControlCharacter(reference.name) || !NAMED_DATA_KINDS.includes(reference.kind) || !NAMED_DATA_SCOPES.includes(reference.scope) || !NAMED_DATA_REPRESENTATIONS.includes(representation)) throw invalidReference$1();
|
|
1347
|
+
if (reference.schemaRef !== void 0) validateSchemaRef(reference.kind, reference.schemaRef);
|
|
530
1348
|
}
|
|
531
1349
|
function validateMetadata(metadata, reference, representation) {
|
|
532
1350
|
if (metadata.representation !== representation || !isNativeRepresentation(metadata.reference.kind, metadata.nativeRepresentation) || metadata.reference.namespace !== reference.namespace || metadata.reference.name !== reference.name || metadata.reference.kind !== reference.kind || metadata.reference.scope !== reference.scope || typeof metadata.mediaType !== "string" || metadata.mediaType.length === 0 || typeof metadata.revision !== "string" || metadata.revision.length === 0 || typeof metadata.replayable !== "boolean" || metadata.byteLength !== void 0 && (!Number.isSafeInteger(metadata.byteLength) || metadata.byteLength < 0) || metadata.digest !== void 0 && !/^sha256-[A-Za-z0-9_-]+$/u.test(metadata.digest)) throw new NamedDataError("NAMED_DATA_INVALID_METADATA", "Provider returned invalid metadata.");
|
|
1351
|
+
const expectedSchema = reference.schemaRef;
|
|
1352
|
+
const actualSchema = metadata.schemaRef;
|
|
1353
|
+
if (actualSchema !== void 0) validateSchemaRef(metadata.reference.kind, actualSchema, "NAMED_DATA_INVALID_METADATA");
|
|
1354
|
+
if (metadata.reference.schemaRef !== void 0) validateSchemaRef(metadata.reference.kind, metadata.reference.schemaRef, "NAMED_DATA_INVALID_METADATA");
|
|
1355
|
+
if (!sameSchemaRef(expectedSchema, actualSchema) || !sameSchemaRef(expectedSchema, metadata.reference.schemaRef)) throw new NamedDataError("NAMED_DATA_SCHEMA_VERSION_MISMATCH", "Provider schemaRef does not match the requested reference.");
|
|
1356
|
+
}
|
|
1357
|
+
function validateSchemaRef(kind, schemaRef, errorCode = "NAMED_DATA_INVALID_REF") {
|
|
1358
|
+
if (kind !== "structured" || typeof schemaRef !== "object" || schemaRef === null || !NAMED_DATA_SCHEMA_ID_PATTERN.test(String(schemaRef.id ?? "")) || !NAMED_DATA_SCHEMA_VERSION_PATTERN.test(String(schemaRef.version ?? "")) || Object.keys(schemaRef).some((key) => key !== "id" && key !== "version")) throw new NamedDataError(errorCode, "Invalid schemaRef; schema references require a structured value, stable id, and full SemVer version.");
|
|
1359
|
+
}
|
|
1360
|
+
function sameSchemaRef(left, right) {
|
|
1361
|
+
return left === void 0 ? right === void 0 : right !== void 0 && left.id === right.id && left.version === right.version;
|
|
533
1362
|
}
|
|
534
1363
|
function isNativeRepresentation(kind, representation) {
|
|
535
1364
|
if (kind === "structured") return representation === "json" || representation === "yaml";
|
|
@@ -566,7 +1395,10 @@ function configuredFlag$1(name) {
|
|
|
566
1395
|
const value = globalThis.__TW_NAMED_DATA_FEATURE_FLAGS__?.[name];
|
|
567
1396
|
return value === true || value === "true";
|
|
568
1397
|
}
|
|
569
|
-
Object.freeze({
|
|
1398
|
+
Object.freeze({
|
|
1399
|
+
NAMED_DATA_REGISTRY_MVP: configuredFlag$1("NAMED_DATA_REGISTRY_MVP"),
|
|
1400
|
+
NAMED_DATA_SCHEMA_REF: configuredFlag$1("NAMED_DATA_SCHEMA_REF")
|
|
1401
|
+
});
|
|
570
1402
|
//#endregion
|
|
571
1403
|
//#region src/asset-cache-error.ts
|
|
572
1404
|
var AssetManagerError = class extends Error {
|
|
@@ -948,88 +1780,6 @@ function throwIfNamedBodyAborted(signal) {
|
|
|
948
1780
|
if (signal?.aborted) throw namedBodyAbortError();
|
|
949
1781
|
}
|
|
950
1782
|
//#endregion
|
|
951
|
-
//#region src/text-style.ts
|
|
952
|
-
var TEXT_RUNTIME_NAMESPACE = "text";
|
|
953
|
-
var TEXT_STYLE_RUNTIME_NAMESPACE = "textStyle";
|
|
954
|
-
var TEXT_STYLE_PROPERTIES = [
|
|
955
|
-
"animation",
|
|
956
|
-
"font",
|
|
957
|
-
"color",
|
|
958
|
-
"width",
|
|
959
|
-
"align"
|
|
960
|
-
];
|
|
961
|
-
var DEFAULT_STAGE_WIDTH = 480;
|
|
962
|
-
var DEFAULT_FONT = "Handwriting";
|
|
963
|
-
var DEFAULT_COLOR = "#ffffff";
|
|
964
|
-
var DEFAULT_ALIGNMENT = "center";
|
|
965
|
-
var DEFAULT_OUTLINE_COLOR = "#000000";
|
|
966
|
-
function textRuntimeVariableName(name) {
|
|
967
|
-
return `${TEXT_RUNTIME_NAMESPACE}:${name}`;
|
|
968
|
-
}
|
|
969
|
-
function textStyleRuntimeVariableName(name, property) {
|
|
970
|
-
return `${TEXT_STYLE_RUNTIME_NAMESPACE}:${name}:${property}`;
|
|
971
|
-
}
|
|
972
|
-
function normalizeTextStyleProperty(value) {
|
|
973
|
-
const property = String(value ?? "").trim().toLowerCase();
|
|
974
|
-
if (TEXT_STYLE_PROPERTIES.includes(property)) return property;
|
|
975
|
-
throw new Error(`Unknown text style property: ${property || "(empty)"}`);
|
|
976
|
-
}
|
|
977
|
-
/**
|
|
978
|
-
* Normalize a DSL-facing style value for runtime-variable storage.
|
|
979
|
-
* An empty value intentionally resets the property to its default.
|
|
980
|
-
*/
|
|
981
|
-
function normalizeTextStyleValue(property, value) {
|
|
982
|
-
const raw = String(value ?? "").trim();
|
|
983
|
-
if (!raw) return "";
|
|
984
|
-
switch (property) {
|
|
985
|
-
case "animation": {
|
|
986
|
-
const animation = raw.toLowerCase() === "typing" ? "type" : raw.toLowerCase();
|
|
987
|
-
if (animation === "none" || animation === "type" || animation === "rainbow" || animation === "zoom" || animation === "shake") return animation;
|
|
988
|
-
throw new Error(`Invalid text animation: ${raw}`);
|
|
989
|
-
}
|
|
990
|
-
case "font": return raw;
|
|
991
|
-
case "color": {
|
|
992
|
-
if (/^#[0-9a-f]{6}$/i.test(raw)) return raw.toLowerCase();
|
|
993
|
-
const shortColor = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(raw);
|
|
994
|
-
if (shortColor) return `#${shortColor[1]}${shortColor[1]}${shortColor[2]}${shortColor[2]}${shortColor[3]}${shortColor[3]}`.toLowerCase();
|
|
995
|
-
throw new Error(`Invalid text color: ${raw}`);
|
|
996
|
-
}
|
|
997
|
-
case "width": {
|
|
998
|
-
const width = Number(raw);
|
|
999
|
-
if (!Number.isFinite(width) || width <= 0) throw new Error(`Text width must be a positive number: ${raw}`);
|
|
1000
|
-
return String(width);
|
|
1001
|
-
}
|
|
1002
|
-
case "align": {
|
|
1003
|
-
const align = raw.toLowerCase();
|
|
1004
|
-
if (align === "left" || align === "center" || align === "right") return align;
|
|
1005
|
-
throw new Error(`Invalid text alignment: ${raw}`);
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
function resolveTextStyle(name, stageWidth, getRuntimeVariable) {
|
|
1010
|
-
const width = Number(stageWidth);
|
|
1011
|
-
const defaults = {
|
|
1012
|
-
animation: "none",
|
|
1013
|
-
font: DEFAULT_FONT,
|
|
1014
|
-
color: DEFAULT_COLOR,
|
|
1015
|
-
width: Number.isFinite(width) && width > 0 ? width : DEFAULT_STAGE_WIDTH,
|
|
1016
|
-
align: DEFAULT_ALIGNMENT
|
|
1017
|
-
};
|
|
1018
|
-
const read = (property) => normalizeTextStyleValue(property, getRuntimeVariable(textStyleRuntimeVariableName(name, property)));
|
|
1019
|
-
const animation = read("animation");
|
|
1020
|
-
const font = read("font");
|
|
1021
|
-
const color = read("color");
|
|
1022
|
-
const configuredWidth = read("width");
|
|
1023
|
-
const align = read("align");
|
|
1024
|
-
return {
|
|
1025
|
-
animation: animation ? animation : defaults.animation,
|
|
1026
|
-
font: font || defaults.font,
|
|
1027
|
-
color: color || defaults.color,
|
|
1028
|
-
width: configuredWidth ? Number(configuredWidth) : defaults.width,
|
|
1029
|
-
align: align ? align : defaults.align
|
|
1030
|
-
};
|
|
1031
|
-
}
|
|
1032
|
-
//#endregion
|
|
1033
1783
|
//#region src/extension.ts
|
|
1034
1784
|
var EXTENSION_ID = "kubohiroyaassetcache";
|
|
1035
1785
|
var EXTENSION_VERSION = "0.14.0";
|
|
@@ -1493,6 +2243,7 @@ var AssetManagerExtension = class {
|
|
|
1493
2243
|
textAssets = /* @__PURE__ */ new Map();
|
|
1494
2244
|
assetRegistry = /* @__PURE__ */ new Map();
|
|
1495
2245
|
displayedAssets = /* @__PURE__ */ new Map();
|
|
2246
|
+
explicitAudio = /* @__PURE__ */ new WeakSet();
|
|
1496
2247
|
playingAudio = /* @__PURE__ */ new Map();
|
|
1497
2248
|
audioVoiceStops = /* @__PURE__ */ new WeakMap();
|
|
1498
2249
|
registrationVersions = /* @__PURE__ */ new Map();
|
|
@@ -1534,6 +2285,7 @@ var AssetManagerExtension = class {
|
|
|
1534
2285
|
this.unbindNamedDataRegistryLifecycle?.();
|
|
1535
2286
|
this.unbindNamedDataRegistryLifecycle = void 0;
|
|
1536
2287
|
};
|
|
2288
|
+
fixedEditors = fixedInputRegistration(Scratch);
|
|
1537
2289
|
constructor(featureFlags = FEATURE_FLAGS) {
|
|
1538
2290
|
this.featureFlags = Object.freeze({ ...featureFlags });
|
|
1539
2291
|
if (this.featureFlags.NAMED_ASSET_BODY_PROVIDER) {
|
|
@@ -1600,6 +2352,8 @@ var AssetManagerExtension = class {
|
|
|
1600
2352
|
return loadingCostumes[((Number.isFinite(numericIndex) ? Math.max(1, Math.trunc(numericIndex)) : 1) - 1) % loadingCostumes.length];
|
|
1601
2353
|
}
|
|
1602
2354
|
getInfo() {
|
|
2355
|
+
const blocks = blockDefinitions.map((block) => this.toScratchBlock(block));
|
|
2356
|
+
this.fixedEditors.ensure(blocks.map((block) => `${EXTENSION_ID}_${block.opcode}`));
|
|
1603
2357
|
return {
|
|
1604
2358
|
id: EXTENSION_ID,
|
|
1605
2359
|
name: Scratch.translate(block_definitions_default.extensionName),
|
|
@@ -1608,9 +2362,13 @@ var AssetManagerExtension = class {
|
|
|
1608
2362
|
color1: "#5b7cfa",
|
|
1609
2363
|
color2: "#425ed8",
|
|
1610
2364
|
color3: "#2f46aa",
|
|
1611
|
-
blocks
|
|
2365
|
+
blocks
|
|
1612
2366
|
};
|
|
1613
2367
|
}
|
|
2368
|
+
/** Release only this optional editor registration; runtime data remains unchanged. */
|
|
2369
|
+
dispose() {
|
|
2370
|
+
this.fixedEditors.dispose();
|
|
2371
|
+
}
|
|
1614
2372
|
/**
|
|
1615
2373
|
* Exposes the stock extension registry through a host-neutral DOM image
|
|
1616
2374
|
* resource contract for other unsandboxed extensions.
|
|
@@ -1858,6 +2616,50 @@ var AssetManagerExtension = class {
|
|
|
1858
2616
|
await playback.started;
|
|
1859
2617
|
return playback.voice;
|
|
1860
2618
|
}
|
|
2619
|
+
async createAudioPlayback(source, options = {}, onDispose = () => {}) {
|
|
2620
|
+
validateAudioPlaybackOptions(options);
|
|
2621
|
+
if (!source || typeof source !== "object" || Array.isArray(source) || Object.keys(source).length !== 1 || Object.hasOwn(source, "asset") === Object.hasOwn(source, "url")) throw new TypeError("Audio requires exactly one asset or URL.");
|
|
2622
|
+
if (Object.hasOwn(source, "url")) {
|
|
2623
|
+
if (typeof source.url !== "string" || !/^https?:\/\/[^\s]+$/u.test(source.url)) throw new TypeError("Audio URL must use HTTP(S) without credentials.");
|
|
2624
|
+
const url = new URL(source.url);
|
|
2625
|
+
if (!url.hostname || url.username || url.password) throw new TypeError("Invalid audio URL.");
|
|
2626
|
+
return this.prepareBrowserAudio("", source.url, options, onDispose, "createAudioPlayback");
|
|
2627
|
+
}
|
|
2628
|
+
const name = normalizeName(source.asset);
|
|
2629
|
+
const version = this.registrationVersions.get(name);
|
|
2630
|
+
const resource = await this.resolveAudioBytes(name);
|
|
2631
|
+
if (!this.assetRegistry.has(name) || version !== this.registrationVersions.get(name)) throw new Error("Audio source was released or replaced while preparing.");
|
|
2632
|
+
const bytes = resource.bytes instanceof Uint8Array ? new Uint8Array(resource.bytes).buffer : resource.bytes;
|
|
2633
|
+
const url = URL.createObjectURL(new Blob([bytes], { type: resource.mimeType }));
|
|
2634
|
+
return this.prepareBrowserAudio(name, url, options, () => {
|
|
2635
|
+
URL.revokeObjectURL(url);
|
|
2636
|
+
onDispose();
|
|
2637
|
+
}, "createAudioPlayback");
|
|
2638
|
+
}
|
|
2639
|
+
prepareBrowserAudio(name, url, options, release, operation) {
|
|
2640
|
+
let audio;
|
|
2641
|
+
let controller;
|
|
2642
|
+
try {
|
|
2643
|
+
const directAudio = operation === "createAudioPlayback" && /^https?:/u.test(url);
|
|
2644
|
+
audio = directAudio ? new Audio() : new Audio(url);
|
|
2645
|
+
if (directAudio) {
|
|
2646
|
+
audio.crossOrigin = "anonymous";
|
|
2647
|
+
audio.src = url;
|
|
2648
|
+
}
|
|
2649
|
+
controller = createBrowserAudioPlayback(audio, options, () => {
|
|
2650
|
+
this.playingAudio.delete(audio);
|
|
2651
|
+
this.audioVoiceStops.delete(audio);
|
|
2652
|
+
release();
|
|
2653
|
+
}, (cause) => this.playbackError(name, cause, operation));
|
|
2654
|
+
} catch (cause) {
|
|
2655
|
+
release();
|
|
2656
|
+
throw this.playbackError(name, cause, operation);
|
|
2657
|
+
}
|
|
2658
|
+
if (operation === "createAudioPlayback") this.explicitAudio.add(audio);
|
|
2659
|
+
this.playingAudio.set(audio, name);
|
|
2660
|
+
this.audioVoiceStops.set(audio, () => controller.dispose());
|
|
2661
|
+
return controller;
|
|
2662
|
+
}
|
|
1861
2663
|
stopSound(args) {
|
|
1862
2664
|
const name = normalizeName(args.NAME);
|
|
1863
2665
|
const kind = this.assetRegistry.get(name);
|
|
@@ -1867,12 +2669,12 @@ var AssetManagerExtension = class {
|
|
|
1867
2669
|
if (!asset) throw this.assetNotRegistered("stopSound", name);
|
|
1868
2670
|
asset.mimeType = normalizeMimeType(asset.mimeType, asset.url || name);
|
|
1869
2671
|
if (!asset.mimeType.startsWith("audio/")) throw this.assetTypeMismatch("stopSound", name, "audio", `external/${this.externalMediaKind(asset)}`);
|
|
1870
|
-
for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name) this.stopExternalAudio(audio);
|
|
2672
|
+
for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name && !this.explicitAudio.has(audio)) this.stopExternalAudio(audio);
|
|
1871
2673
|
return;
|
|
1872
2674
|
}
|
|
1873
2675
|
if (kind === "sound") {
|
|
1874
2676
|
const { target, sound, soundBank } = this.resolveSoundReference(name);
|
|
1875
|
-
for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name) this.stopExternalAudio(audio);
|
|
2677
|
+
for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name && !this.explicitAudio.has(audio)) this.stopExternalAudio(audio);
|
|
1876
2678
|
soundBank.stop(target, sound.soundId);
|
|
1877
2679
|
return;
|
|
1878
2680
|
}
|
|
@@ -2120,6 +2922,7 @@ var AssetManagerExtension = class {
|
|
|
2120
2922
|
}
|
|
2121
2923
|
unregisterAsset(name) {
|
|
2122
2924
|
this.cancelRegistrations(name);
|
|
2925
|
+
for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name) this.stopExternalAudio(audio);
|
|
2123
2926
|
this.invalidateDOMImageResources(name);
|
|
2124
2927
|
const kind = this.assetRegistry.get(name);
|
|
2125
2928
|
if (!kind) return;
|
|
@@ -2935,73 +3738,33 @@ var AssetManagerExtension = class {
|
|
|
2935
3738
|
}
|
|
2936
3739
|
startBrowserAudioVoice(name, bytes, mimeType, initialGain, operation) {
|
|
2937
3740
|
const blobBytes = bytes instanceof Uint8Array ? new Uint8Array(bytes).buffer : bytes;
|
|
2938
|
-
const
|
|
2939
|
-
|
|
2940
|
-
try {
|
|
2941
|
-
audio = new Audio(objectUrl);
|
|
2942
|
-
audio.volume = initialGain;
|
|
2943
|
-
} catch (error) {
|
|
2944
|
-
URL.revokeObjectURL(objectUrl);
|
|
2945
|
-
throw this.playbackError(name, error, operation);
|
|
2946
|
-
}
|
|
3741
|
+
const url = URL.createObjectURL(new Blob([blobBytes], { type: mimeType }));
|
|
3742
|
+
const controller = this.prepareBrowserAudio(name, url, { gain: initialGain }, () => URL.revokeObjectURL(url), operation);
|
|
2947
3743
|
let active = true;
|
|
2948
|
-
let resolveEnded;
|
|
2949
|
-
let rejectEnded;
|
|
2950
|
-
const ended = new Promise((resolve, reject) => {
|
|
2951
|
-
resolveEnded = resolve;
|
|
2952
|
-
rejectEnded = reject;
|
|
2953
|
-
});
|
|
2954
|
-
ended.catch(() => {});
|
|
2955
|
-
const cleanup = (error) => {
|
|
2956
|
-
if (!active) return;
|
|
2957
|
-
active = false;
|
|
2958
|
-
audio.removeEventListener("ended", handleEnded);
|
|
2959
|
-
audio.removeEventListener("error", handleError);
|
|
2960
|
-
this.playingAudio.delete(audio);
|
|
2961
|
-
this.audioVoiceStops.delete(audio);
|
|
2962
|
-
URL.revokeObjectURL(objectUrl);
|
|
2963
|
-
if (error === void 0) resolveEnded();
|
|
2964
|
-
else rejectEnded(error);
|
|
2965
|
-
};
|
|
2966
|
-
const handleEnded = () => cleanup();
|
|
2967
|
-
const handleError = () => cleanup(operation === "createAudioVoice" ? this.playbackError(name, /* @__PURE__ */ new Error("The browser audio element reported a playback error."), operation) : void 0);
|
|
2968
3744
|
const stop = () => {
|
|
2969
3745
|
if (!active) return;
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
audio.currentTime = 0;
|
|
2973
|
-
} catch {}
|
|
2974
|
-
cleanup();
|
|
3746
|
+
active = false;
|
|
3747
|
+
controller.dispose();
|
|
2975
3748
|
};
|
|
2976
|
-
const
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
if (active) audio.volume = gain;
|
|
2981
|
-
},
|
|
2982
|
-
stop
|
|
3749
|
+
const started = controller.play();
|
|
3750
|
+
const ended = controller.ended.then(() => stop(), (error) => {
|
|
3751
|
+
stop();
|
|
3752
|
+
if (operation === "createAudioVoice") throw error;
|
|
2983
3753
|
});
|
|
2984
|
-
|
|
2985
|
-
audio.addEventListener("error", handleError, { once: true });
|
|
2986
|
-
this.playingAudio.set(audio, name);
|
|
2987
|
-
this.audioVoiceStops.set(audio, stop);
|
|
2988
|
-
let playResult;
|
|
2989
|
-
try {
|
|
2990
|
-
playResult = audio.play();
|
|
2991
|
-
} catch (error) {
|
|
2992
|
-
const playbackError = this.playbackError(name, error, operation);
|
|
2993
|
-
cleanup(playbackError);
|
|
2994
|
-
return {
|
|
2995
|
-
voice,
|
|
2996
|
-
started: Promise.reject(playbackError)
|
|
2997
|
-
};
|
|
2998
|
-
}
|
|
3754
|
+
ended.catch(() => {});
|
|
2999
3755
|
return {
|
|
3000
|
-
voice
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3756
|
+
voice: Object.freeze({
|
|
3757
|
+
ended,
|
|
3758
|
+
setGain(value) {
|
|
3759
|
+
const gain = normalizeAudioVoiceGain(value);
|
|
3760
|
+
if (active) controller.setGain(gain);
|
|
3761
|
+
},
|
|
3762
|
+
stop
|
|
3763
|
+
}),
|
|
3764
|
+
started: started.catch((error) => {
|
|
3765
|
+
const legacyStop = operation === "playSound" && !active && error instanceof Error && error.name === "AbortError";
|
|
3766
|
+
stop();
|
|
3767
|
+
if (!legacyStop) throw error;
|
|
3005
3768
|
})
|
|
3006
3769
|
};
|
|
3007
3770
|
}
|
|
@@ -8041,6 +8804,8 @@ function createAssetManagerComposition(featureFlags, options = {}) {
|
|
|
8041
8804
|
}
|
|
8042
8805
|
});
|
|
8043
8806
|
}
|
|
8807
|
+
const audioPlaybacks = /* @__PURE__ */ new Set();
|
|
8808
|
+
let audioGeneration = 0;
|
|
8044
8809
|
function releaseAsset(name) {
|
|
8045
8810
|
const owned = ownedName(name);
|
|
8046
8811
|
if (!owned) return;
|
|
@@ -8221,7 +8986,13 @@ function createAssetManagerComposition(featureFlags, options = {}) {
|
|
|
8221
8986
|
releaseAsset,
|
|
8222
8987
|
releaseAll() {
|
|
8223
8988
|
releaseAllDOMImageResources();
|
|
8989
|
+
audioGeneration++;
|
|
8224
8990
|
const errors = [];
|
|
8991
|
+
for (const playback of audioPlaybacks) try {
|
|
8992
|
+
playback.dispose();
|
|
8993
|
+
} catch (error) {
|
|
8994
|
+
errors.push(error);
|
|
8995
|
+
}
|
|
8225
8996
|
for (const name of [...ownedNames.keys()].reverse()) try {
|
|
8226
8997
|
releaseAsset(name);
|
|
8227
8998
|
} catch (error) {
|
|
@@ -8267,6 +9038,21 @@ function createAssetManagerComposition(featureFlags, options = {}) {
|
|
|
8267
9038
|
createAudioVoice(name, options) {
|
|
8268
9039
|
return extension.createAudioVoice(ownedName(name)?.internal ?? name, options);
|
|
8269
9040
|
},
|
|
9041
|
+
async createAudioPlayback(source, options) {
|
|
9042
|
+
if (!source || typeof source !== "object" || Array.isArray(source) || Object.keys(source).length !== 1) throw new TypeError("Invalid audio source.");
|
|
9043
|
+
const generation = audioGeneration;
|
|
9044
|
+
const normalizedSource = Object.hasOwn(source, "asset") ? { asset: ownedName(source.asset)?.internal ?? source.asset } : source;
|
|
9045
|
+
let playback;
|
|
9046
|
+
playback = await extension.createAudioPlayback(normalizedSource, options, () => {
|
|
9047
|
+
if (playback) audioPlaybacks.delete(playback);
|
|
9048
|
+
});
|
|
9049
|
+
if (generation !== audioGeneration) {
|
|
9050
|
+
playback.dispose();
|
|
9051
|
+
throw new Error("Audio composition was released while preparing.");
|
|
9052
|
+
}
|
|
9053
|
+
audioPlaybacks.add(playback);
|
|
9054
|
+
return playback;
|
|
9055
|
+
},
|
|
8270
9056
|
stopSound(name) {
|
|
8271
9057
|
extension.stopSound({ NAME: ownedName(name)?.internal ?? name });
|
|
8272
9058
|
},
|