@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/asset-cache.js
CHANGED
|
@@ -7,6 +7,421 @@
|
|
|
7
7
|
(function (Scratch) {
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
|
+
//#region src/audio-output.ts
|
|
11
|
+
/** Media volume handles attenuation and fades; this shared graph adds amplification. */
|
|
12
|
+
var shared;
|
|
13
|
+
var maximumFloat = 34028234663852886e22;
|
|
14
|
+
function createAudioAmplifier(audio) {
|
|
15
|
+
if (typeof globalThis.AudioContext !== "function") throw new Error("Audio amplification requires Web Audio support.");
|
|
16
|
+
if (!shared || shared.context.state === "closed") shared = {
|
|
17
|
+
context: new AudioContext(),
|
|
18
|
+
users: 0
|
|
19
|
+
};
|
|
20
|
+
const owner = shared;
|
|
21
|
+
owner.users++;
|
|
22
|
+
let source;
|
|
23
|
+
let gain;
|
|
24
|
+
let released = false;
|
|
25
|
+
const dispose = () => {
|
|
26
|
+
if (released) return;
|
|
27
|
+
released = true;
|
|
28
|
+
source?.disconnect();
|
|
29
|
+
gain?.disconnect();
|
|
30
|
+
owner.users--;
|
|
31
|
+
if (owner.users === 0) {
|
|
32
|
+
if (shared === owner) shared = void 0;
|
|
33
|
+
owner.context.close().catch(() => {});
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
source = owner.context.createMediaElementSource(audio);
|
|
38
|
+
gain = owner.context.createGain();
|
|
39
|
+
source.connect(gain);
|
|
40
|
+
gain.connect(owner.context.destination);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
dispose();
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
const node = gain;
|
|
46
|
+
return {
|
|
47
|
+
setGain(value) {
|
|
48
|
+
node.gain.value = Math.min(maximumFloat, Math.max(1, value));
|
|
49
|
+
},
|
|
50
|
+
resume() {
|
|
51
|
+
return owner.context.state === "running" ? Promise.resolve() : owner.context.resume();
|
|
52
|
+
},
|
|
53
|
+
dispose
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/audio-playback.ts
|
|
58
|
+
function playbackGain(value) {
|
|
59
|
+
const gain = value === void 0 ? 1 : value;
|
|
60
|
+
if (typeof gain !== "number" || !Number.isFinite(gain) || gain < 0) throw new TypeError("Audio gain must be a finite non-negative number.");
|
|
61
|
+
return gain;
|
|
62
|
+
}
|
|
63
|
+
function rate(value) {
|
|
64
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < .25 || value > 4) throw new TypeError("Audio playback rate must be from 0.25 to 4.");
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function validateAudioPlaybackOptions(options) {
|
|
68
|
+
if (!options || typeof options !== "object" || Array.isArray(options) || Object.keys(options).some((key) => ![
|
|
69
|
+
"gain",
|
|
70
|
+
"muted",
|
|
71
|
+
"playbackRate",
|
|
72
|
+
"fadeIn",
|
|
73
|
+
"fadeOut"
|
|
74
|
+
].includes(key))) throw new TypeError("Invalid audio playback options.");
|
|
75
|
+
playbackGain(options.gain);
|
|
76
|
+
if (options.muted !== void 0 && typeof options.muted !== "boolean") throw new TypeError("Audio muted must be a boolean.");
|
|
77
|
+
rate(options.playbackRate ?? 1);
|
|
78
|
+
fadeSeconds(options.fadeIn, "fadeIn");
|
|
79
|
+
fadeSeconds(options.fadeOut, "fadeOut");
|
|
80
|
+
}
|
|
81
|
+
function fadeSeconds(value, label) {
|
|
82
|
+
const seconds = value === void 0 ? 0 : value;
|
|
83
|
+
if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 0) throw new TypeError(`Audio ${label} must be a finite non-negative number of seconds.`);
|
|
84
|
+
return seconds;
|
|
85
|
+
}
|
|
86
|
+
function aborted() {
|
|
87
|
+
const error = /* @__PURE__ */ new Error("Audio playback was cancelled.");
|
|
88
|
+
error.name = "AbortError";
|
|
89
|
+
return error;
|
|
90
|
+
}
|
|
91
|
+
/** One reusable native element; the caller owns source resolution and resource release. */
|
|
92
|
+
function createBrowserAudioPlayback(audio, options, release, playbackError) {
|
|
93
|
+
validateAudioPlaybackOptions(options);
|
|
94
|
+
let gain = playbackGain(options.gain);
|
|
95
|
+
audio.volume = Math.min(1, gain);
|
|
96
|
+
audio.muted = options.muted ?? false;
|
|
97
|
+
audio.playbackRate = options.playbackRate ?? 1;
|
|
98
|
+
audio.loop = false;
|
|
99
|
+
const defaultFadeIn = fadeSeconds(options.fadeIn, "fadeIn");
|
|
100
|
+
const defaultFadeOut = fadeSeconds(options.fadeOut, "fadeOut");
|
|
101
|
+
let activeFadeIn = defaultFadeIn;
|
|
102
|
+
let activeFadeOut = defaultFadeOut;
|
|
103
|
+
let envelope = 1;
|
|
104
|
+
let fadeStarted = 0;
|
|
105
|
+
let playing = false;
|
|
106
|
+
let fadeTimer;
|
|
107
|
+
let stopping;
|
|
108
|
+
let amplifier;
|
|
109
|
+
const pendingGain = /* @__PURE__ */ new Set();
|
|
110
|
+
let disposed = false;
|
|
111
|
+
let generation = 0;
|
|
112
|
+
let ended = Promise.resolve();
|
|
113
|
+
let settle;
|
|
114
|
+
let rejectStart;
|
|
115
|
+
let detachAbort;
|
|
116
|
+
const pendingMetadata = /* @__PURE__ */ new Set();
|
|
117
|
+
const requireActive = () => {
|
|
118
|
+
if (disposed) throw new Error("Audio playback is disposed.");
|
|
119
|
+
};
|
|
120
|
+
const clearFadeTimer = () => {
|
|
121
|
+
if (fadeTimer !== void 0) clearTimeout(fadeTimer);
|
|
122
|
+
fadeTimer = void 0;
|
|
123
|
+
};
|
|
124
|
+
const finish = (error) => {
|
|
125
|
+
playing = false;
|
|
126
|
+
for (const reject of pendingGain) reject(error ?? aborted());
|
|
127
|
+
pendingGain.clear();
|
|
128
|
+
clearFadeTimer();
|
|
129
|
+
const previousStop = stopping;
|
|
130
|
+
stopping = void 0;
|
|
131
|
+
previousStop?.detach();
|
|
132
|
+
previousStop?.reject(error ?? aborted());
|
|
133
|
+
generation++;
|
|
134
|
+
rejectStart?.(error ?? aborted());
|
|
135
|
+
rejectStart = void 0;
|
|
136
|
+
detachAbort?.();
|
|
137
|
+
detachAbort = void 0;
|
|
138
|
+
settle?.(error);
|
|
139
|
+
settle = void 0;
|
|
140
|
+
};
|
|
141
|
+
const writeVolume = () => {
|
|
142
|
+
audio.volume = Math.min(1, gain) * envelope;
|
|
143
|
+
amplifier?.setGain(gain);
|
|
144
|
+
};
|
|
145
|
+
const prepareAmplifier = () => {
|
|
146
|
+
if (!amplifier && gain > 1) amplifier = createAudioAmplifier(audio);
|
|
147
|
+
writeVolume();
|
|
148
|
+
};
|
|
149
|
+
const stopNow = () => {
|
|
150
|
+
const completed = stopping;
|
|
151
|
+
stopping = void 0;
|
|
152
|
+
completed?.detach();
|
|
153
|
+
audio.pause();
|
|
154
|
+
audio.loop = false;
|
|
155
|
+
audio.currentTime = 0;
|
|
156
|
+
finish();
|
|
157
|
+
envelope = 1;
|
|
158
|
+
writeVolume();
|
|
159
|
+
completed?.resolve();
|
|
160
|
+
};
|
|
161
|
+
const refreshFade = () => {
|
|
162
|
+
clearFadeTimer();
|
|
163
|
+
if (disposed || !playing && !stopping) return;
|
|
164
|
+
const now = performance.now();
|
|
165
|
+
let delay = 16;
|
|
166
|
+
if (stopping) {
|
|
167
|
+
const progress = Math.min(1, (now - stopping.started) / (stopping.seconds * 1e3));
|
|
168
|
+
envelope = stopping.from * (1 - progress);
|
|
169
|
+
writeVolume();
|
|
170
|
+
if (progress >= 1) {
|
|
171
|
+
stopNow();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
const fadeIn = activeFadeIn === 0 ? 1 : Math.min(1, (now - fadeStarted) / (activeFadeIn * 1e3));
|
|
176
|
+
const remaining = Number.isFinite(audio.duration) ? Math.max(0, audio.duration - audio.currentTime) / audio.playbackRate : Infinity;
|
|
177
|
+
const fadeOut = !audio.loop && activeFadeOut > 0 ? Math.min(1, remaining / activeFadeOut) : 1;
|
|
178
|
+
envelope = Math.max(0, Math.min(fadeIn, fadeOut));
|
|
179
|
+
writeVolume();
|
|
180
|
+
if (fadeIn >= 1) {
|
|
181
|
+
if (audio.loop || activeFadeOut === 0) return;
|
|
182
|
+
if (!Number.isFinite(remaining)) delay = 250;
|
|
183
|
+
else if (remaining > activeFadeOut) delay = Math.min(250, Math.max(16, (remaining - activeFadeOut) * 1e3));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
fadeTimer = setTimeout(refreshFade, delay);
|
|
187
|
+
};
|
|
188
|
+
const onEnded = () => {
|
|
189
|
+
if (stopping) {
|
|
190
|
+
stopNow();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
finish();
|
|
194
|
+
if (activeFadeOut > 0 && !audio.loop) {
|
|
195
|
+
envelope = 0;
|
|
196
|
+
writeVolume();
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const onError = () => {
|
|
200
|
+
audio.pause();
|
|
201
|
+
const error = playbackError(audio.error ?? /* @__PURE__ */ new Error("The browser audio element reported a playback error."));
|
|
202
|
+
finish(error);
|
|
203
|
+
for (const complete of pendingMetadata) complete(error);
|
|
204
|
+
};
|
|
205
|
+
audio.addEventListener("ended", onEnded);
|
|
206
|
+
audio.addEventListener("error", onError);
|
|
207
|
+
for (const event of [
|
|
208
|
+
"durationchange",
|
|
209
|
+
"ratechange",
|
|
210
|
+
"seeked"
|
|
211
|
+
]) audio.addEventListener(event, refreshFade);
|
|
212
|
+
return Object.freeze({
|
|
213
|
+
get ended() {
|
|
214
|
+
return ended;
|
|
215
|
+
},
|
|
216
|
+
get currentTime() {
|
|
217
|
+
return audio.currentTime;
|
|
218
|
+
},
|
|
219
|
+
get paused() {
|
|
220
|
+
return audio.paused;
|
|
221
|
+
},
|
|
222
|
+
get loop() {
|
|
223
|
+
return audio.loop;
|
|
224
|
+
},
|
|
225
|
+
get gain() {
|
|
226
|
+
return gain;
|
|
227
|
+
},
|
|
228
|
+
get muted() {
|
|
229
|
+
return audio.muted;
|
|
230
|
+
},
|
|
231
|
+
get playbackRate() {
|
|
232
|
+
return audio.playbackRate;
|
|
233
|
+
},
|
|
234
|
+
async play(playOptions = {}) {
|
|
235
|
+
requireActive();
|
|
236
|
+
if (!playOptions || typeof playOptions !== "object" || Array.isArray(playOptions) || Object.keys(playOptions).some((key) => ![
|
|
237
|
+
"restart",
|
|
238
|
+
"loop",
|
|
239
|
+
"signal",
|
|
240
|
+
"fadeIn",
|
|
241
|
+
"fadeOut",
|
|
242
|
+
"gain"
|
|
243
|
+
].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.");
|
|
244
|
+
const fadeIn = fadeSeconds(playOptions.fadeIn === void 0 ? defaultFadeIn : playOptions.fadeIn, "fadeIn");
|
|
245
|
+
const fadeOut = fadeSeconds(playOptions.fadeOut === void 0 ? defaultFadeOut : playOptions.fadeOut, "fadeOut");
|
|
246
|
+
const nextGain = playOptions.gain === void 0 ? gain : playbackGain(playOptions.gain);
|
|
247
|
+
const signal = playOptions.signal;
|
|
248
|
+
if (signal?.aborted) throw aborted();
|
|
249
|
+
finish();
|
|
250
|
+
const run = generation;
|
|
251
|
+
gain = nextGain;
|
|
252
|
+
if (playOptions.restart) audio.currentTime = 0;
|
|
253
|
+
audio.loop = playOptions.loop ?? audio.loop;
|
|
254
|
+
activeFadeIn = fadeIn;
|
|
255
|
+
activeFadeOut = fadeOut;
|
|
256
|
+
envelope = fadeIn > 0 ? 0 : 1;
|
|
257
|
+
writeVolume();
|
|
258
|
+
ended = new Promise((resolve, reject) => {
|
|
259
|
+
settle = (error) => error === void 0 ? resolve() : reject(error);
|
|
260
|
+
});
|
|
261
|
+
ended.catch(() => {});
|
|
262
|
+
const onAbort = () => {
|
|
263
|
+
audio.pause();
|
|
264
|
+
finish(aborted());
|
|
265
|
+
};
|
|
266
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
267
|
+
detachAbort = () => signal?.removeEventListener("abort", onAbort);
|
|
268
|
+
try {
|
|
269
|
+
prepareAmplifier();
|
|
270
|
+
const started = audio.play();
|
|
271
|
+
const ready = amplifier ? Promise.all([started, amplifier.resume()]) : Promise.resolve(started);
|
|
272
|
+
await Promise.race([ready, new Promise((_resolve, reject) => {
|
|
273
|
+
rejectStart = reject;
|
|
274
|
+
})]);
|
|
275
|
+
if (run !== generation) throw aborted();
|
|
276
|
+
rejectStart = void 0;
|
|
277
|
+
playing = true;
|
|
278
|
+
fadeStarted = performance.now();
|
|
279
|
+
refreshFade();
|
|
280
|
+
} catch (cause) {
|
|
281
|
+
if (run !== generation) throw cause;
|
|
282
|
+
audio.pause();
|
|
283
|
+
const error = playbackError(cause);
|
|
284
|
+
finish(error);
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
pause() {
|
|
289
|
+
requireActive();
|
|
290
|
+
audio.pause();
|
|
291
|
+
finish();
|
|
292
|
+
},
|
|
293
|
+
stop(stopOptions = {}) {
|
|
294
|
+
requireActive();
|
|
295
|
+
if (!stopOptions || typeof stopOptions !== "object" || Array.isArray(stopOptions) || Object.keys(stopOptions).some((key) => !["fadeOut", "signal"].includes(key))) throw new TypeError("Invalid audio stop options.");
|
|
296
|
+
const seconds = fadeSeconds(stopOptions.fadeOut === void 0 ? activeFadeOut : stopOptions.fadeOut, "fadeOut");
|
|
297
|
+
const signal = stopOptions.signal;
|
|
298
|
+
if (signal?.aborted) throw aborted();
|
|
299
|
+
if (seconds === 0 || audio.paused || !playing) {
|
|
300
|
+
stopNow();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const previous = stopping;
|
|
304
|
+
stopping = void 0;
|
|
305
|
+
previous?.detach();
|
|
306
|
+
previous?.reject(aborted());
|
|
307
|
+
return new Promise((resolve, reject) => {
|
|
308
|
+
const onAbort = () => {
|
|
309
|
+
const current = stopping;
|
|
310
|
+
stopping = void 0;
|
|
311
|
+
current?.detach();
|
|
312
|
+
stopNow();
|
|
313
|
+
reject(aborted());
|
|
314
|
+
};
|
|
315
|
+
stopping = {
|
|
316
|
+
started: performance.now(),
|
|
317
|
+
seconds,
|
|
318
|
+
from: envelope,
|
|
319
|
+
resolve,
|
|
320
|
+
reject,
|
|
321
|
+
detach: () => signal?.removeEventListener("abort", onAbort)
|
|
322
|
+
};
|
|
323
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
324
|
+
if (signal?.aborted) onAbort();
|
|
325
|
+
else refreshFade();
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
async seek(seconds, signal) {
|
|
329
|
+
requireActive();
|
|
330
|
+
if (!Number.isFinite(seconds) || seconds < 0) throw new TypeError("Audio seek time must be non-negative.");
|
|
331
|
+
if (signal?.aborted) throw aborted();
|
|
332
|
+
if (audio.readyState < 1) await new Promise((resolve, reject) => {
|
|
333
|
+
const complete = (error) => {
|
|
334
|
+
clearTimeout(timer);
|
|
335
|
+
audio.removeEventListener("loadedmetadata", ready);
|
|
336
|
+
signal?.removeEventListener("abort", cancel);
|
|
337
|
+
pendingMetadata.delete(complete);
|
|
338
|
+
if (error === void 0) resolve();
|
|
339
|
+
else reject(error);
|
|
340
|
+
};
|
|
341
|
+
const ready = () => complete();
|
|
342
|
+
const cancel = () => complete(aborted());
|
|
343
|
+
const timer = setTimeout(() => complete(/* @__PURE__ */ new Error("Audio metadata did not become ready.")), 1e4);
|
|
344
|
+
pendingMetadata.add(complete);
|
|
345
|
+
audio.addEventListener("loadedmetadata", ready, { once: true });
|
|
346
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
347
|
+
if (signal?.aborted) cancel();
|
|
348
|
+
else if (audio.readyState >= 1) ready();
|
|
349
|
+
});
|
|
350
|
+
requireActive();
|
|
351
|
+
if (signal?.aborted) throw aborted();
|
|
352
|
+
if (Number.isFinite(audio.duration) && seconds > audio.duration) throw new RangeError("Audio seek exceeds its duration.");
|
|
353
|
+
audio.currentTime = seconds;
|
|
354
|
+
refreshFade();
|
|
355
|
+
},
|
|
356
|
+
setGain(value) {
|
|
357
|
+
const normalized = playbackGain(value);
|
|
358
|
+
requireActive();
|
|
359
|
+
const previous = gain;
|
|
360
|
+
gain = normalized;
|
|
361
|
+
try {
|
|
362
|
+
if (playing) prepareAmplifier();
|
|
363
|
+
else writeVolume();
|
|
364
|
+
} catch (error) {
|
|
365
|
+
gain = previous;
|
|
366
|
+
writeVolume();
|
|
367
|
+
throw playbackError(error);
|
|
368
|
+
}
|
|
369
|
+
if (playing && amplifier) {
|
|
370
|
+
const run = generation;
|
|
371
|
+
let rejectResume;
|
|
372
|
+
const cancelled = new Promise((_resolve, reject) => {
|
|
373
|
+
rejectResume = reject;
|
|
374
|
+
});
|
|
375
|
+
pendingGain.add(rejectResume);
|
|
376
|
+
return Promise.race([amplifier.resume(), cancelled]).catch((cause) => {
|
|
377
|
+
if (run !== generation) throw cause;
|
|
378
|
+
audio.pause();
|
|
379
|
+
const error = playbackError(cause);
|
|
380
|
+
finish(error);
|
|
381
|
+
throw error;
|
|
382
|
+
}).finally(() => pendingGain.delete(rejectResume));
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
setMuted(value) {
|
|
386
|
+
if (typeof value !== "boolean") throw new TypeError("Audio muted must be a boolean.");
|
|
387
|
+
requireActive();
|
|
388
|
+
audio.muted = value;
|
|
389
|
+
},
|
|
390
|
+
setPlaybackRate(value) {
|
|
391
|
+
const normalized = rate(value);
|
|
392
|
+
requireActive();
|
|
393
|
+
audio.playbackRate = normalized;
|
|
394
|
+
refreshFade();
|
|
395
|
+
},
|
|
396
|
+
dispose() {
|
|
397
|
+
if (disposed) return;
|
|
398
|
+
disposed = true;
|
|
399
|
+
try {
|
|
400
|
+
audio.pause();
|
|
401
|
+
audio.currentTime = 0;
|
|
402
|
+
} catch {}
|
|
403
|
+
finish();
|
|
404
|
+
for (const complete of pendingMetadata) complete(aborted());
|
|
405
|
+
audio.removeEventListener("ended", onEnded);
|
|
406
|
+
audio.removeEventListener("error", onError);
|
|
407
|
+
for (const event of [
|
|
408
|
+
"durationchange",
|
|
409
|
+
"ratechange",
|
|
410
|
+
"seeked"
|
|
411
|
+
]) audio.removeEventListener(event, refreshFade);
|
|
412
|
+
try {
|
|
413
|
+
audio.removeAttribute?.("src");
|
|
414
|
+
audio.load?.();
|
|
415
|
+
} finally {
|
|
416
|
+
try {
|
|
417
|
+
amplifier?.dispose();
|
|
418
|
+
} finally {
|
|
419
|
+
release();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
}
|
|
10
425
|
var block_definitions_default = {
|
|
11
426
|
extensionName: "Asset Cache",
|
|
12
427
|
blocks: [
|
|
@@ -287,7 +702,404 @@
|
|
|
287
702
|
]
|
|
288
703
|
};
|
|
289
704
|
//#endregion
|
|
290
|
-
//#region node_modules/.pnpm/@kubohiroya+turbowarp-
|
|
705
|
+
//#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
|
|
706
|
+
var numberToken = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/u;
|
|
707
|
+
var invalidNumber = Object.freeze({
|
|
708
|
+
kind: "invalid",
|
|
709
|
+
code: "fixed-number-input-invalid"
|
|
710
|
+
});
|
|
711
|
+
/**
|
|
712
|
+
* Parse a NEW numeric draft according to NP-finite-new-input-v1.
|
|
713
|
+
* Never apply this to opening/no-op raw values or use it as an owner validator.
|
|
714
|
+
* Keep the validated token so negative zero, precision and exponent spelling survive.
|
|
715
|
+
*/
|
|
716
|
+
function parseFixedNumberInput(input) {
|
|
717
|
+
if (typeof input === "number") {
|
|
718
|
+
if (!Number.isFinite(input)) return invalidNumber;
|
|
719
|
+
return Object.freeze({
|
|
720
|
+
kind: "valid",
|
|
721
|
+
token: Object.is(input, -0) ? "-0" : String(input),
|
|
722
|
+
value: input
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
if (typeof input !== "string") return invalidNumber;
|
|
726
|
+
const token = input.trim();
|
|
727
|
+
if (!numberToken.test(token)) return invalidNumber;
|
|
728
|
+
const value = Number(token);
|
|
729
|
+
return Number.isFinite(value) ? Object.freeze({
|
|
730
|
+
kind: "valid",
|
|
731
|
+
token,
|
|
732
|
+
value
|
|
733
|
+
}) : invalidNumber;
|
|
734
|
+
}
|
|
735
|
+
var block_editor_fixed_inputs_schema_default = {
|
|
736
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
737
|
+
$id: "https://kubohiroya.github.io/turbowarp-asset-cache/schemas/block-editor-fixed-inputs.schema.json",
|
|
738
|
+
$defs: {
|
|
739
|
+
"setTextStyle": {
|
|
740
|
+
"type": "object",
|
|
741
|
+
"properties": {
|
|
742
|
+
"PROPERTY": {
|
|
743
|
+
"type": "string",
|
|
744
|
+
"enum": [
|
|
745
|
+
"animation",
|
|
746
|
+
"font",
|
|
747
|
+
"color",
|
|
748
|
+
"width",
|
|
749
|
+
"align"
|
|
750
|
+
]
|
|
751
|
+
},
|
|
752
|
+
"VALUE": { "type": "string" }
|
|
753
|
+
},
|
|
754
|
+
"additionalProperties": false
|
|
755
|
+
},
|
|
756
|
+
"animation": {
|
|
757
|
+
"type": "string",
|
|
758
|
+
"enum": [
|
|
759
|
+
"",
|
|
760
|
+
"none",
|
|
761
|
+
"type",
|
|
762
|
+
"rainbow",
|
|
763
|
+
"zoom",
|
|
764
|
+
"shake"
|
|
765
|
+
]
|
|
766
|
+
},
|
|
767
|
+
"font": { "type": "string" },
|
|
768
|
+
"color": {
|
|
769
|
+
"type": "string",
|
|
770
|
+
"pattern": "^(?:|#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6})$"
|
|
771
|
+
},
|
|
772
|
+
"width": {
|
|
773
|
+
"type": "string",
|
|
774
|
+
"$comment": "Empty/trimmed empty resets; otherwise normalizeTextStyleValue requires Number(value) finite and positive. Preserve the existing STRING grammar, including hexadecimal and leading plus."
|
|
775
|
+
},
|
|
776
|
+
"align": {
|
|
777
|
+
"type": "string",
|
|
778
|
+
"enum": [
|
|
779
|
+
"",
|
|
780
|
+
"left",
|
|
781
|
+
"center",
|
|
782
|
+
"right"
|
|
783
|
+
]
|
|
784
|
+
},
|
|
785
|
+
"loadingCostumeAt": {
|
|
786
|
+
"type": "object",
|
|
787
|
+
"properties": { "INDEX": { "type": "number" } },
|
|
788
|
+
"additionalProperties": false
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
//#endregion
|
|
793
|
+
//#region src/fixed-input-schema.ts
|
|
794
|
+
/** Canonical owner schema fragments; no network resources or runtime providers. */
|
|
795
|
+
function fixedInputBundle(fragment, schema) {
|
|
796
|
+
const source = {
|
|
797
|
+
repository: "turbowarp-asset-cache",
|
|
798
|
+
path: "schemas/block-editor-fixed-inputs.schema.json",
|
|
799
|
+
symbol: {
|
|
800
|
+
"setTextStyle-PROPERTY": "#/$defs/setTextStyle/properties/PROPERTY",
|
|
801
|
+
"setTextStyle-VALUE": "#/$defs/setTextStyle/properties/VALUE",
|
|
802
|
+
"loadingCostumeAt-INDEX": "#/$defs/loadingCostumeAt/properties/INDEX"
|
|
803
|
+
}[fragment] ?? `#/$defs/${fragment}`
|
|
804
|
+
};
|
|
805
|
+
return {
|
|
806
|
+
identity: {
|
|
807
|
+
ownerId: "turbowarp-asset-cache",
|
|
808
|
+
contractId: "fixed-inputs",
|
|
809
|
+
revision: "1",
|
|
810
|
+
variantId: fragment
|
|
811
|
+
},
|
|
812
|
+
dialect: "2020-12",
|
|
813
|
+
root: {
|
|
814
|
+
retrievalUri: block_editor_fixed_inputs_schema_default.$id,
|
|
815
|
+
document: schema,
|
|
816
|
+
artifact: {
|
|
817
|
+
role: "canonical-schema",
|
|
818
|
+
source
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
resources: [],
|
|
822
|
+
provenance: {
|
|
823
|
+
canonicalSources: [
|
|
824
|
+
source,
|
|
825
|
+
{
|
|
826
|
+
repository: "turbowarp-asset-cache",
|
|
827
|
+
path: "src/text-style.ts",
|
|
828
|
+
symbol: "normalizeTextStyleProperty / normalizeTextStyleValue"
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
repository: "turbowarp-asset-cache",
|
|
832
|
+
path: "src/extension.ts",
|
|
833
|
+
symbol: "getInfo / loadingCostumeAt"
|
|
834
|
+
}
|
|
835
|
+
],
|
|
836
|
+
generatedFiles: [],
|
|
837
|
+
referenceSchemas: [],
|
|
838
|
+
generationSources: []
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
var fixedSchemas = block_editor_fixed_inputs_schema_default.$defs;
|
|
843
|
+
//#endregion
|
|
844
|
+
//#region src/text-style.ts
|
|
845
|
+
var TEXT_RUNTIME_NAMESPACE = "text";
|
|
846
|
+
var TEXT_STYLE_RUNTIME_NAMESPACE = "textStyle";
|
|
847
|
+
var TEXT_STYLE_PROPERTIES = [
|
|
848
|
+
"animation",
|
|
849
|
+
"font",
|
|
850
|
+
"color",
|
|
851
|
+
"width",
|
|
852
|
+
"align"
|
|
853
|
+
];
|
|
854
|
+
var DEFAULT_STAGE_WIDTH = 480;
|
|
855
|
+
var DEFAULT_FONT = "Handwriting";
|
|
856
|
+
var DEFAULT_COLOR = "#ffffff";
|
|
857
|
+
var DEFAULT_ALIGNMENT = "center";
|
|
858
|
+
var DEFAULT_OUTLINE_COLOR = "#000000";
|
|
859
|
+
function textRuntimeVariableName(name) {
|
|
860
|
+
return `${TEXT_RUNTIME_NAMESPACE}:${name}`;
|
|
861
|
+
}
|
|
862
|
+
function textStyleRuntimeVariableName(name, property) {
|
|
863
|
+
return `${TEXT_STYLE_RUNTIME_NAMESPACE}:${name}:${property}`;
|
|
864
|
+
}
|
|
865
|
+
function normalizeTextStyleProperty(value) {
|
|
866
|
+
const property = String(value ?? "").trim().toLowerCase();
|
|
867
|
+
if (TEXT_STYLE_PROPERTIES.includes(property)) return property;
|
|
868
|
+
throw new Error(`Unknown text style property: ${property || "(empty)"}`);
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Normalize a DSL-facing style value for runtime-variable storage.
|
|
872
|
+
* An empty value intentionally resets the property to its default.
|
|
873
|
+
*/
|
|
874
|
+
function normalizeTextStyleValue(property, value) {
|
|
875
|
+
const raw = String(value ?? "").trim();
|
|
876
|
+
if (!raw) return "";
|
|
877
|
+
switch (property) {
|
|
878
|
+
case "animation": {
|
|
879
|
+
const animation = raw.toLowerCase() === "typing" ? "type" : raw.toLowerCase();
|
|
880
|
+
if (animation === "none" || animation === "type" || animation === "rainbow" || animation === "zoom" || animation === "shake") return animation;
|
|
881
|
+
throw new Error(`Invalid text animation: ${raw}`);
|
|
882
|
+
}
|
|
883
|
+
case "font": return raw;
|
|
884
|
+
case "color": {
|
|
885
|
+
if (/^#[0-9a-f]{6}$/i.test(raw)) return raw.toLowerCase();
|
|
886
|
+
const shortColor = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(raw);
|
|
887
|
+
if (shortColor) return `#${shortColor[1]}${shortColor[1]}${shortColor[2]}${shortColor[2]}${shortColor[3]}${shortColor[3]}`.toLowerCase();
|
|
888
|
+
throw new Error(`Invalid text color: ${raw}`);
|
|
889
|
+
}
|
|
890
|
+
case "width": {
|
|
891
|
+
const width = Number(raw);
|
|
892
|
+
if (!Number.isFinite(width) || width <= 0) throw new Error(`Text width must be a positive number: ${raw}`);
|
|
893
|
+
return String(width);
|
|
894
|
+
}
|
|
895
|
+
case "align": {
|
|
896
|
+
const align = raw.toLowerCase();
|
|
897
|
+
if (align === "left" || align === "center" || align === "right") return align;
|
|
898
|
+
throw new Error(`Invalid text alignment: ${raw}`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
function resolveTextStyle(name, stageWidth, getRuntimeVariable) {
|
|
903
|
+
const width = Number(stageWidth);
|
|
904
|
+
const defaults = {
|
|
905
|
+
animation: "none",
|
|
906
|
+
font: DEFAULT_FONT,
|
|
907
|
+
color: DEFAULT_COLOR,
|
|
908
|
+
width: Number.isFinite(width) && width > 0 ? width : DEFAULT_STAGE_WIDTH,
|
|
909
|
+
align: DEFAULT_ALIGNMENT
|
|
910
|
+
};
|
|
911
|
+
const read = (property) => normalizeTextStyleValue(property, getRuntimeVariable(textStyleRuntimeVariableName(name, property)));
|
|
912
|
+
const animation = read("animation");
|
|
913
|
+
const font = read("font");
|
|
914
|
+
const color = read("color");
|
|
915
|
+
const configuredWidth = read("width");
|
|
916
|
+
const align = read("align");
|
|
917
|
+
return {
|
|
918
|
+
animation: animation ? animation : defaults.animation,
|
|
919
|
+
font: font || defaults.font,
|
|
920
|
+
color: color || defaults.color,
|
|
921
|
+
width: configuredWidth ? Number(configuredWidth) : defaults.width,
|
|
922
|
+
align: align ? align : defaults.align
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
//#endregion
|
|
926
|
+
//#region src/fixed-input-editors.ts
|
|
927
|
+
var extensionId = "kubohiroyaassetcache";
|
|
928
|
+
var choices = (values) => values.map((value) => ({ value }));
|
|
929
|
+
var context = () => ({
|
|
930
|
+
consumerId: "turbowarp-asset-cache",
|
|
931
|
+
nowMs: 0,
|
|
932
|
+
settings: null
|
|
933
|
+
});
|
|
934
|
+
var string = (value) => {
|
|
935
|
+
if (typeof value !== "string") throw new TypeError("Expected a string literal.");
|
|
936
|
+
return value;
|
|
937
|
+
};
|
|
938
|
+
var issue = () => [{ message: "Invalid asset style value." }];
|
|
939
|
+
/** New edits preserve the candidate token; existing aliases are display projections only. */
|
|
940
|
+
function styleBinding(property) {
|
|
941
|
+
const values = property === "animation" ? [
|
|
942
|
+
"",
|
|
943
|
+
"none",
|
|
944
|
+
"type",
|
|
945
|
+
"rainbow",
|
|
946
|
+
"zoom",
|
|
947
|
+
"shake"
|
|
948
|
+
] : property === "align" ? [
|
|
949
|
+
"",
|
|
950
|
+
"left",
|
|
951
|
+
"center",
|
|
952
|
+
"right"
|
|
953
|
+
] : void 0;
|
|
954
|
+
return {
|
|
955
|
+
argument: "VALUE",
|
|
956
|
+
schemaBundle: fixedInputBundle(property, fixedSchemas[property]),
|
|
957
|
+
control: {
|
|
958
|
+
kind: values ? "enum" : property === "color" ? "color" : property === "width" ? "number" : "text",
|
|
959
|
+
reset: "",
|
|
960
|
+
...values ? { choices: choices(values) } : {}
|
|
961
|
+
},
|
|
962
|
+
read(value) {
|
|
963
|
+
return normalizeTextStyleValue(property, string(value));
|
|
964
|
+
},
|
|
965
|
+
encode(value) {
|
|
966
|
+
const raw = string(value);
|
|
967
|
+
if (property !== "width" || raw === "") return raw;
|
|
968
|
+
normalizeTextStyleValue(property, raw);
|
|
969
|
+
return raw;
|
|
970
|
+
},
|
|
971
|
+
validateCandidate(value) {
|
|
972
|
+
try {
|
|
973
|
+
normalizeTextStyleValue(property, string(value));
|
|
974
|
+
return [];
|
|
975
|
+
} catch {
|
|
976
|
+
return issue();
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
var fixedInputEditors = [{
|
|
982
|
+
apiVersion: 1,
|
|
983
|
+
extensionId,
|
|
984
|
+
editorId: "text-style-fixed",
|
|
985
|
+
opcodes: [`${extensionId}_setTextStyle`],
|
|
986
|
+
contractId: "CP-asset-cache",
|
|
987
|
+
contractRevision: "1",
|
|
988
|
+
validationContext: context,
|
|
989
|
+
bindings: [{
|
|
990
|
+
argument: "PROPERTY",
|
|
991
|
+
schemaBundle: fixedInputBundle("setTextStyle-PROPERTY", fixedSchemas.setTextStyle.properties.PROPERTY),
|
|
992
|
+
control: {
|
|
993
|
+
kind: "enum",
|
|
994
|
+
choices: choices(TEXT_STYLE_PROPERTIES)
|
|
995
|
+
},
|
|
996
|
+
read(value) {
|
|
997
|
+
const raw = string(value);
|
|
998
|
+
try {
|
|
999
|
+
return normalizeTextStyleProperty(raw);
|
|
1000
|
+
} catch {
|
|
1001
|
+
return raw;
|
|
1002
|
+
}
|
|
1003
|
+
},
|
|
1004
|
+
encode: (value) => string(value),
|
|
1005
|
+
validateCandidate(value) {
|
|
1006
|
+
try {
|
|
1007
|
+
normalizeTextStyleProperty(value);
|
|
1008
|
+
return [];
|
|
1009
|
+
} catch {
|
|
1010
|
+
return issue();
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}, {
|
|
1014
|
+
argument: "VALUE",
|
|
1015
|
+
schemaBundle: fixedInputBundle("setTextStyle-VALUE", fixedSchemas.setTextStyle.properties.VALUE),
|
|
1016
|
+
read: (value) => string(value),
|
|
1017
|
+
encode: (value) => string(value),
|
|
1018
|
+
validateCandidate: () => []
|
|
1019
|
+
}],
|
|
1020
|
+
dependencies: [{
|
|
1021
|
+
selector: "PROPERTY",
|
|
1022
|
+
dependents: ["VALUE"]
|
|
1023
|
+
}],
|
|
1024
|
+
branchPolicies: [{
|
|
1025
|
+
selector: "PROPERTY",
|
|
1026
|
+
normalize: {
|
|
1027
|
+
trim: true,
|
|
1028
|
+
lowercase: true
|
|
1029
|
+
},
|
|
1030
|
+
cases: TEXT_STYLE_PROPERTIES.map((property) => ({
|
|
1031
|
+
values: [property],
|
|
1032
|
+
activeDependents: ["VALUE"],
|
|
1033
|
+
bindings: [styleBinding(property)]
|
|
1034
|
+
}))
|
|
1035
|
+
}]
|
|
1036
|
+
}, {
|
|
1037
|
+
apiVersion: 1,
|
|
1038
|
+
extensionId,
|
|
1039
|
+
editorId: "loading-costume-index-fixed",
|
|
1040
|
+
opcodes: [`${extensionId}_loadingCostumeAt`],
|
|
1041
|
+
contractId: "turbowarp-asset-cache/loadingCostumeAt.INDEX",
|
|
1042
|
+
contractRevision: "1",
|
|
1043
|
+
validationContext: context,
|
|
1044
|
+
bindings: [{
|
|
1045
|
+
argument: "INDEX",
|
|
1046
|
+
schemaBundle: fixedInputBundle("loadingCostumeAt-INDEX", fixedSchemas.loadingCostumeAt.properties.INDEX),
|
|
1047
|
+
control: { kind: "number" },
|
|
1048
|
+
read(value) {
|
|
1049
|
+
if (typeof value !== "number" && typeof value !== "string" || typeof value === "string" && value.trim() === "") throw new TypeError("Non-numeric index.");
|
|
1050
|
+
const number = Number(value);
|
|
1051
|
+
if (!Number.isFinite(number)) throw new TypeError("Nonfinite index.");
|
|
1052
|
+
return number;
|
|
1053
|
+
},
|
|
1054
|
+
encode(value) {
|
|
1055
|
+
const parsed = parseFixedNumberInput(value);
|
|
1056
|
+
if (parsed.kind !== "valid") throw new TypeError("Invalid index.");
|
|
1057
|
+
return parsed.token;
|
|
1058
|
+
},
|
|
1059
|
+
validateCandidate(value) {
|
|
1060
|
+
return Number.isFinite(Number(value)) ? [] : [{ message: "Invalid index." }];
|
|
1061
|
+
}
|
|
1062
|
+
}]
|
|
1063
|
+
}];
|
|
1064
|
+
/** Per-instance, idempotent registration. Legacy hosts never acquire the fixed capability. */
|
|
1065
|
+
function fixedInputRegistration(scratch) {
|
|
1066
|
+
const host = scratch;
|
|
1067
|
+
let active;
|
|
1068
|
+
let removers = [];
|
|
1069
|
+
let disposed = false;
|
|
1070
|
+
const dispose = () => {
|
|
1071
|
+
host.vm?.runtime?.off?.("RUNTIME_DISPOSED", shutdown);
|
|
1072
|
+
for (const remove of removers.splice(0).reverse()) remove();
|
|
1073
|
+
active = void 0;
|
|
1074
|
+
};
|
|
1075
|
+
const shutdown = () => {
|
|
1076
|
+
disposed = true;
|
|
1077
|
+
dispose();
|
|
1078
|
+
};
|
|
1079
|
+
return {
|
|
1080
|
+
ensure(opcodes) {
|
|
1081
|
+
const api = host.gui?.blockEditors;
|
|
1082
|
+
if (disposed || api === active) return;
|
|
1083
|
+
dispose();
|
|
1084
|
+
if (api?.apiVersion !== 1 || typeof api.registerFixedInputEditor !== "function") return;
|
|
1085
|
+
try {
|
|
1086
|
+
for (const definition of fixedInputEditors) if (definition.opcodes.every((opcode) => opcodes.includes(opcode))) {
|
|
1087
|
+
const remove = api.registerFixedInputEditor.call(api, definition);
|
|
1088
|
+
if (typeof remove !== "function") throw new TypeError("Invalid fixed registration cleanup.");
|
|
1089
|
+
removers.push(remove);
|
|
1090
|
+
}
|
|
1091
|
+
active = api;
|
|
1092
|
+
host.vm?.runtime?.on?.("RUNTIME_DISPOSED", shutdown);
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
dispose();
|
|
1095
|
+
throw error;
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
dispose: shutdown
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
//#endregion
|
|
1102
|
+
//#region node_modules/.pnpm/@kubohiroya+turbowarp-named-data@0.3.1/node_modules/@kubohiroya/turbowarp-named-data/dist/composition.js
|
|
291
1103
|
var NAMED_DATA_REGISTRY_SYMBOL_KEY = "@kubohiroya/turbowarp-named-data/registry";
|
|
292
1104
|
var NAMED_DATA_REGISTRY_SYMBOL = Symbol.for(NAMED_DATA_REGISTRY_SYMBOL_KEY);
|
|
293
1105
|
var NAMED_DATA_NAMESPACE_PATTERN = /^[a-z][a-z0-9.-]{0,63}$/u;
|
|
@@ -308,6 +1120,8 @@
|
|
|
308
1120
|
"markdown",
|
|
309
1121
|
"raw"
|
|
310
1122
|
];
|
|
1123
|
+
var NAMED_DATA_SCHEMA_ID_PATTERN = /^[a-z][a-z0-9.-]{0,127}$/u;
|
|
1124
|
+
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;
|
|
311
1125
|
var NAMED_DATA_ERROR_CODES = [
|
|
312
1126
|
"NAMED_DATA_INVALID_REF",
|
|
313
1127
|
"NAMED_DATA_INVALID_REGISTRY",
|
|
@@ -316,6 +1130,9 @@
|
|
|
316
1130
|
"NAMED_DATA_NOT_FOUND",
|
|
317
1131
|
"NAMED_DATA_KIND_MISMATCH",
|
|
318
1132
|
"NAMED_DATA_SCOPE_MISMATCH",
|
|
1133
|
+
"NAMED_DATA_SCHEMA_NOT_FOUND",
|
|
1134
|
+
"NAMED_DATA_SCHEMA_VERSION_MISMATCH",
|
|
1135
|
+
"NAMED_DATA_SCHEMA_UNRESOLVED",
|
|
319
1136
|
"NAMED_DATA_REPRESENTATION_UNSUPPORTED",
|
|
320
1137
|
"NAMED_DATA_INVALID_METADATA",
|
|
321
1138
|
"NAMED_DATA_BODY_TOO_LARGE",
|
|
@@ -536,9 +1353,21 @@
|
|
|
536
1353
|
if (!reference || typeof reference !== "object") throw invalidReference$1();
|
|
537
1354
|
requireNamespace(reference.namespace);
|
|
538
1355
|
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();
|
|
1356
|
+
if (reference.schemaRef !== void 0) validateSchemaRef(reference.kind, reference.schemaRef);
|
|
539
1357
|
}
|
|
540
1358
|
function validateMetadata(metadata, reference, representation) {
|
|
541
1359
|
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.");
|
|
1360
|
+
const expectedSchema = reference.schemaRef;
|
|
1361
|
+
const actualSchema = metadata.schemaRef;
|
|
1362
|
+
if (actualSchema !== void 0) validateSchemaRef(metadata.reference.kind, actualSchema, "NAMED_DATA_INVALID_METADATA");
|
|
1363
|
+
if (metadata.reference.schemaRef !== void 0) validateSchemaRef(metadata.reference.kind, metadata.reference.schemaRef, "NAMED_DATA_INVALID_METADATA");
|
|
1364
|
+
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.");
|
|
1365
|
+
}
|
|
1366
|
+
function validateSchemaRef(kind, schemaRef, errorCode = "NAMED_DATA_INVALID_REF") {
|
|
1367
|
+
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.");
|
|
1368
|
+
}
|
|
1369
|
+
function sameSchemaRef(left, right) {
|
|
1370
|
+
return left === void 0 ? right === void 0 : right !== void 0 && left.id === right.id && left.version === right.version;
|
|
542
1371
|
}
|
|
543
1372
|
function isNativeRepresentation(kind, representation) {
|
|
544
1373
|
if (kind === "structured") return representation === "json" || representation === "yaml";
|
|
@@ -575,7 +1404,10 @@
|
|
|
575
1404
|
const value = globalThis.__TW_NAMED_DATA_FEATURE_FLAGS__?.[name];
|
|
576
1405
|
return value === true || value === "true";
|
|
577
1406
|
}
|
|
578
|
-
Object.freeze({
|
|
1407
|
+
Object.freeze({
|
|
1408
|
+
NAMED_DATA_REGISTRY_MVP: configuredFlag$1("NAMED_DATA_REGISTRY_MVP"),
|
|
1409
|
+
NAMED_DATA_SCHEMA_REF: configuredFlag$1("NAMED_DATA_SCHEMA_REF")
|
|
1410
|
+
});
|
|
579
1411
|
//#endregion
|
|
580
1412
|
//#region \0@oxc-project+runtime@0.148.0/helpers/esm/typeof.js
|
|
581
1413
|
function _typeof(o) {
|
|
@@ -994,88 +1826,6 @@
|
|
|
994
1826
|
if (signal?.aborted) throw namedBodyAbortError();
|
|
995
1827
|
}
|
|
996
1828
|
//#endregion
|
|
997
|
-
//#region src/text-style.ts
|
|
998
|
-
var TEXT_RUNTIME_NAMESPACE = "text";
|
|
999
|
-
var TEXT_STYLE_RUNTIME_NAMESPACE = "textStyle";
|
|
1000
|
-
var TEXT_STYLE_PROPERTIES = [
|
|
1001
|
-
"animation",
|
|
1002
|
-
"font",
|
|
1003
|
-
"color",
|
|
1004
|
-
"width",
|
|
1005
|
-
"align"
|
|
1006
|
-
];
|
|
1007
|
-
var DEFAULT_STAGE_WIDTH = 480;
|
|
1008
|
-
var DEFAULT_FONT = "Handwriting";
|
|
1009
|
-
var DEFAULT_COLOR = "#ffffff";
|
|
1010
|
-
var DEFAULT_ALIGNMENT = "center";
|
|
1011
|
-
var DEFAULT_OUTLINE_COLOR = "#000000";
|
|
1012
|
-
function textRuntimeVariableName(name) {
|
|
1013
|
-
return `${TEXT_RUNTIME_NAMESPACE}:${name}`;
|
|
1014
|
-
}
|
|
1015
|
-
function textStyleRuntimeVariableName(name, property) {
|
|
1016
|
-
return `${TEXT_STYLE_RUNTIME_NAMESPACE}:${name}:${property}`;
|
|
1017
|
-
}
|
|
1018
|
-
function normalizeTextStyleProperty(value) {
|
|
1019
|
-
const property = String(value ?? "").trim().toLowerCase();
|
|
1020
|
-
if (TEXT_STYLE_PROPERTIES.includes(property)) return property;
|
|
1021
|
-
throw new Error(`Unknown text style property: ${property || "(empty)"}`);
|
|
1022
|
-
}
|
|
1023
|
-
/**
|
|
1024
|
-
* Normalize a DSL-facing style value for runtime-variable storage.
|
|
1025
|
-
* An empty value intentionally resets the property to its default.
|
|
1026
|
-
*/
|
|
1027
|
-
function normalizeTextStyleValue(property, value) {
|
|
1028
|
-
const raw = String(value ?? "").trim();
|
|
1029
|
-
if (!raw) return "";
|
|
1030
|
-
switch (property) {
|
|
1031
|
-
case "animation": {
|
|
1032
|
-
const animation = raw.toLowerCase() === "typing" ? "type" : raw.toLowerCase();
|
|
1033
|
-
if (animation === "none" || animation === "type" || animation === "rainbow" || animation === "zoom" || animation === "shake") return animation;
|
|
1034
|
-
throw new Error(`Invalid text animation: ${raw}`);
|
|
1035
|
-
}
|
|
1036
|
-
case "font": return raw;
|
|
1037
|
-
case "color": {
|
|
1038
|
-
if (/^#[0-9a-f]{6}$/i.test(raw)) return raw.toLowerCase();
|
|
1039
|
-
const shortColor = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(raw);
|
|
1040
|
-
if (shortColor) return `#${shortColor[1]}${shortColor[1]}${shortColor[2]}${shortColor[2]}${shortColor[3]}${shortColor[3]}`.toLowerCase();
|
|
1041
|
-
throw new Error(`Invalid text color: ${raw}`);
|
|
1042
|
-
}
|
|
1043
|
-
case "width": {
|
|
1044
|
-
const width = Number(raw);
|
|
1045
|
-
if (!Number.isFinite(width) || width <= 0) throw new Error(`Text width must be a positive number: ${raw}`);
|
|
1046
|
-
return String(width);
|
|
1047
|
-
}
|
|
1048
|
-
case "align": {
|
|
1049
|
-
const align = raw.toLowerCase();
|
|
1050
|
-
if (align === "left" || align === "center" || align === "right") return align;
|
|
1051
|
-
throw new Error(`Invalid text alignment: ${raw}`);
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
function resolveTextStyle(name, stageWidth, getRuntimeVariable) {
|
|
1056
|
-
const width = Number(stageWidth);
|
|
1057
|
-
const defaults = {
|
|
1058
|
-
animation: "none",
|
|
1059
|
-
font: DEFAULT_FONT,
|
|
1060
|
-
color: DEFAULT_COLOR,
|
|
1061
|
-
width: Number.isFinite(width) && width > 0 ? width : DEFAULT_STAGE_WIDTH,
|
|
1062
|
-
align: DEFAULT_ALIGNMENT
|
|
1063
|
-
};
|
|
1064
|
-
const read = (property) => normalizeTextStyleValue(property, getRuntimeVariable(textStyleRuntimeVariableName(name, property)));
|
|
1065
|
-
const animation = read("animation");
|
|
1066
|
-
const font = read("font");
|
|
1067
|
-
const color = read("color");
|
|
1068
|
-
const configuredWidth = read("width");
|
|
1069
|
-
const align = read("align");
|
|
1070
|
-
return {
|
|
1071
|
-
animation: animation ? animation : defaults.animation,
|
|
1072
|
-
font: font || defaults.font,
|
|
1073
|
-
color: color || defaults.color,
|
|
1074
|
-
width: configuredWidth ? Number(configuredWidth) : defaults.width,
|
|
1075
|
-
align: align ? align : defaults.align
|
|
1076
|
-
};
|
|
1077
|
-
}
|
|
1078
|
-
//#endregion
|
|
1079
1829
|
//#region src/extension.ts
|
|
1080
1830
|
var EXTENSION_ID = "kubohiroyaassetcache";
|
|
1081
1831
|
var EXTENSION_VERSION = "0.14.0";
|
|
@@ -1540,6 +2290,7 @@
|
|
|
1540
2290
|
_defineProperty(this, "textAssets", /* @__PURE__ */ new Map());
|
|
1541
2291
|
_defineProperty(this, "assetRegistry", /* @__PURE__ */ new Map());
|
|
1542
2292
|
_defineProperty(this, "displayedAssets", /* @__PURE__ */ new Map());
|
|
2293
|
+
_defineProperty(this, "explicitAudio", /* @__PURE__ */ new WeakSet());
|
|
1543
2294
|
_defineProperty(this, "playingAudio", /* @__PURE__ */ new Map());
|
|
1544
2295
|
_defineProperty(this, "audioVoiceStops", /* @__PURE__ */ new WeakMap());
|
|
1545
2296
|
_defineProperty(this, "registrationVersions", /* @__PURE__ */ new Map());
|
|
@@ -1581,6 +2332,7 @@
|
|
|
1581
2332
|
this.unbindNamedDataRegistryLifecycle?.();
|
|
1582
2333
|
this.unbindNamedDataRegistryLifecycle = void 0;
|
|
1583
2334
|
});
|
|
2335
|
+
_defineProperty(this, "fixedEditors", fixedInputRegistration(Scratch));
|
|
1584
2336
|
this.featureFlags = Object.freeze({ ...featureFlags });
|
|
1585
2337
|
if (this.featureFlags.NAMED_ASSET_BODY_PROVIDER) {
|
|
1586
2338
|
this.namedBodyProviderValue = this.createNamedBodyProvider();
|
|
@@ -1646,6 +2398,8 @@
|
|
|
1646
2398
|
return loadingCostumes[((Number.isFinite(numericIndex) ? Math.max(1, Math.trunc(numericIndex)) : 1) - 1) % loadingCostumes.length];
|
|
1647
2399
|
}
|
|
1648
2400
|
getInfo() {
|
|
2401
|
+
const blocks = blockDefinitions.map((block) => this.toScratchBlock(block));
|
|
2402
|
+
this.fixedEditors.ensure(blocks.map((block) => `${EXTENSION_ID}_${block.opcode}`));
|
|
1649
2403
|
return {
|
|
1650
2404
|
id: EXTENSION_ID,
|
|
1651
2405
|
name: Scratch.translate(block_definitions_default.extensionName),
|
|
@@ -1654,9 +2408,13 @@
|
|
|
1654
2408
|
color1: "#5b7cfa",
|
|
1655
2409
|
color2: "#425ed8",
|
|
1656
2410
|
color3: "#2f46aa",
|
|
1657
|
-
blocks
|
|
2411
|
+
blocks
|
|
1658
2412
|
};
|
|
1659
2413
|
}
|
|
2414
|
+
/** Release only this optional editor registration; runtime data remains unchanged. */
|
|
2415
|
+
dispose() {
|
|
2416
|
+
this.fixedEditors.dispose();
|
|
2417
|
+
}
|
|
1660
2418
|
/**
|
|
1661
2419
|
* Exposes the stock extension registry through a host-neutral DOM image
|
|
1662
2420
|
* resource contract for other unsandboxed extensions.
|
|
@@ -1904,6 +2662,50 @@
|
|
|
1904
2662
|
await playback.started;
|
|
1905
2663
|
return playback.voice;
|
|
1906
2664
|
}
|
|
2665
|
+
async createAudioPlayback(source, options = {}, onDispose = () => {}) {
|
|
2666
|
+
validateAudioPlaybackOptions(options);
|
|
2667
|
+
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.");
|
|
2668
|
+
if (Object.hasOwn(source, "url")) {
|
|
2669
|
+
if (typeof source.url !== "string" || !/^https?:\/\/[^\s]+$/u.test(source.url)) throw new TypeError("Audio URL must use HTTP(S) without credentials.");
|
|
2670
|
+
const url = new URL(source.url);
|
|
2671
|
+
if (!url.hostname || url.username || url.password) throw new TypeError("Invalid audio URL.");
|
|
2672
|
+
return this.prepareBrowserAudio("", source.url, options, onDispose, "createAudioPlayback");
|
|
2673
|
+
}
|
|
2674
|
+
const name = normalizeName(source.asset);
|
|
2675
|
+
const version = this.registrationVersions.get(name);
|
|
2676
|
+
const resource = await this.resolveAudioBytes(name);
|
|
2677
|
+
if (!this.assetRegistry.has(name) || version !== this.registrationVersions.get(name)) throw new Error("Audio source was released or replaced while preparing.");
|
|
2678
|
+
const bytes = resource.bytes instanceof Uint8Array ? new Uint8Array(resource.bytes).buffer : resource.bytes;
|
|
2679
|
+
const url = URL.createObjectURL(new Blob([bytes], { type: resource.mimeType }));
|
|
2680
|
+
return this.prepareBrowserAudio(name, url, options, () => {
|
|
2681
|
+
URL.revokeObjectURL(url);
|
|
2682
|
+
onDispose();
|
|
2683
|
+
}, "createAudioPlayback");
|
|
2684
|
+
}
|
|
2685
|
+
prepareBrowserAudio(name, url, options, release, operation) {
|
|
2686
|
+
let audio;
|
|
2687
|
+
let controller;
|
|
2688
|
+
try {
|
|
2689
|
+
const directAudio = operation === "createAudioPlayback" && /^https?:/u.test(url);
|
|
2690
|
+
audio = directAudio ? new Audio() : new Audio(url);
|
|
2691
|
+
if (directAudio) {
|
|
2692
|
+
audio.crossOrigin = "anonymous";
|
|
2693
|
+
audio.src = url;
|
|
2694
|
+
}
|
|
2695
|
+
controller = createBrowserAudioPlayback(audio, options, () => {
|
|
2696
|
+
this.playingAudio.delete(audio);
|
|
2697
|
+
this.audioVoiceStops.delete(audio);
|
|
2698
|
+
release();
|
|
2699
|
+
}, (cause) => this.playbackError(name, cause, operation));
|
|
2700
|
+
} catch (cause) {
|
|
2701
|
+
release();
|
|
2702
|
+
throw this.playbackError(name, cause, operation);
|
|
2703
|
+
}
|
|
2704
|
+
if (operation === "createAudioPlayback") this.explicitAudio.add(audio);
|
|
2705
|
+
this.playingAudio.set(audio, name);
|
|
2706
|
+
this.audioVoiceStops.set(audio, () => controller.dispose());
|
|
2707
|
+
return controller;
|
|
2708
|
+
}
|
|
1907
2709
|
stopSound(args) {
|
|
1908
2710
|
const name = normalizeName(args.NAME);
|
|
1909
2711
|
const kind = this.assetRegistry.get(name);
|
|
@@ -1913,12 +2715,12 @@
|
|
|
1913
2715
|
if (!asset) throw this.assetNotRegistered("stopSound", name);
|
|
1914
2716
|
asset.mimeType = normalizeMimeType(asset.mimeType, asset.url || name);
|
|
1915
2717
|
if (!asset.mimeType.startsWith("audio/")) throw this.assetTypeMismatch("stopSound", name, "audio", `external/${this.externalMediaKind(asset)}`);
|
|
1916
|
-
for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name) this.stopExternalAudio(audio);
|
|
2718
|
+
for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name && !this.explicitAudio.has(audio)) this.stopExternalAudio(audio);
|
|
1917
2719
|
return;
|
|
1918
2720
|
}
|
|
1919
2721
|
if (kind === "sound") {
|
|
1920
2722
|
const { target, sound, soundBank } = this.resolveSoundReference(name);
|
|
1921
|
-
for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name) this.stopExternalAudio(audio);
|
|
2723
|
+
for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name && !this.explicitAudio.has(audio)) this.stopExternalAudio(audio);
|
|
1922
2724
|
soundBank.stop(target, sound.soundId);
|
|
1923
2725
|
return;
|
|
1924
2726
|
}
|
|
@@ -2166,6 +2968,7 @@
|
|
|
2166
2968
|
}
|
|
2167
2969
|
unregisterAsset(name) {
|
|
2168
2970
|
this.cancelRegistrations(name);
|
|
2971
|
+
for (const [audio, assetName] of [...this.playingAudio]) if (assetName === name) this.stopExternalAudio(audio);
|
|
2169
2972
|
this.invalidateDOMImageResources(name);
|
|
2170
2973
|
const kind = this.assetRegistry.get(name);
|
|
2171
2974
|
if (!kind) return;
|
|
@@ -2981,73 +3784,33 @@
|
|
|
2981
3784
|
}
|
|
2982
3785
|
startBrowserAudioVoice(name, bytes, mimeType, initialGain, operation) {
|
|
2983
3786
|
const blobBytes = bytes instanceof Uint8Array ? new Uint8Array(bytes).buffer : bytes;
|
|
2984
|
-
const
|
|
2985
|
-
|
|
2986
|
-
try {
|
|
2987
|
-
audio = new Audio(objectUrl);
|
|
2988
|
-
audio.volume = initialGain;
|
|
2989
|
-
} catch (error) {
|
|
2990
|
-
URL.revokeObjectURL(objectUrl);
|
|
2991
|
-
throw this.playbackError(name, error, operation);
|
|
2992
|
-
}
|
|
3787
|
+
const url = URL.createObjectURL(new Blob([blobBytes], { type: mimeType }));
|
|
3788
|
+
const controller = this.prepareBrowserAudio(name, url, { gain: initialGain }, () => URL.revokeObjectURL(url), operation);
|
|
2993
3789
|
let active = true;
|
|
2994
|
-
let resolveEnded;
|
|
2995
|
-
let rejectEnded;
|
|
2996
|
-
const ended = new Promise((resolve, reject) => {
|
|
2997
|
-
resolveEnded = resolve;
|
|
2998
|
-
rejectEnded = reject;
|
|
2999
|
-
});
|
|
3000
|
-
ended.catch(() => {});
|
|
3001
|
-
const cleanup = (error) => {
|
|
3002
|
-
if (!active) return;
|
|
3003
|
-
active = false;
|
|
3004
|
-
audio.removeEventListener("ended", handleEnded);
|
|
3005
|
-
audio.removeEventListener("error", handleError);
|
|
3006
|
-
this.playingAudio.delete(audio);
|
|
3007
|
-
this.audioVoiceStops.delete(audio);
|
|
3008
|
-
URL.revokeObjectURL(objectUrl);
|
|
3009
|
-
if (error === void 0) resolveEnded();
|
|
3010
|
-
else rejectEnded(error);
|
|
3011
|
-
};
|
|
3012
|
-
const handleEnded = () => cleanup();
|
|
3013
|
-
const handleError = () => cleanup(operation === "createAudioVoice" ? this.playbackError(name, /* @__PURE__ */ new Error("The browser audio element reported a playback error."), operation) : void 0);
|
|
3014
3790
|
const stop = () => {
|
|
3015
3791
|
if (!active) return;
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
audio.currentTime = 0;
|
|
3019
|
-
} catch {}
|
|
3020
|
-
cleanup();
|
|
3792
|
+
active = false;
|
|
3793
|
+
controller.dispose();
|
|
3021
3794
|
};
|
|
3022
|
-
const
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
if (active) audio.volume = gain;
|
|
3027
|
-
},
|
|
3028
|
-
stop
|
|
3795
|
+
const started = controller.play();
|
|
3796
|
+
const ended = controller.ended.then(() => stop(), (error) => {
|
|
3797
|
+
stop();
|
|
3798
|
+
if (operation === "createAudioVoice") throw error;
|
|
3029
3799
|
});
|
|
3030
|
-
|
|
3031
|
-
audio.addEventListener("error", handleError, { once: true });
|
|
3032
|
-
this.playingAudio.set(audio, name);
|
|
3033
|
-
this.audioVoiceStops.set(audio, stop);
|
|
3034
|
-
let playResult;
|
|
3035
|
-
try {
|
|
3036
|
-
playResult = audio.play();
|
|
3037
|
-
} catch (error) {
|
|
3038
|
-
const playbackError = this.playbackError(name, error, operation);
|
|
3039
|
-
cleanup(playbackError);
|
|
3040
|
-
return {
|
|
3041
|
-
voice,
|
|
3042
|
-
started: Promise.reject(playbackError)
|
|
3043
|
-
};
|
|
3044
|
-
}
|
|
3800
|
+
ended.catch(() => {});
|
|
3045
3801
|
return {
|
|
3046
|
-
voice
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3802
|
+
voice: Object.freeze({
|
|
3803
|
+
ended,
|
|
3804
|
+
setGain(value) {
|
|
3805
|
+
const gain = normalizeAudioVoiceGain(value);
|
|
3806
|
+
if (active) controller.setGain(gain);
|
|
3807
|
+
},
|
|
3808
|
+
stop
|
|
3809
|
+
}),
|
|
3810
|
+
started: started.catch((error) => {
|
|
3811
|
+
const legacyStop = operation === "playSound" && !active && error instanceof Error && error.name === "AbortError";
|
|
3812
|
+
stop();
|
|
3813
|
+
if (!legacyStop) throw error;
|
|
3051
3814
|
})
|
|
3052
3815
|
};
|
|
3053
3816
|
}
|