@neta-art/cohub 12.5.0 → 13.0.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 +35 -13
- package/dist/board/editor/editor.d.ts +7 -5
- package/dist/board/editor/index.js +1 -1
- package/dist/board/editor/interaction.js +1 -1
- package/dist/board/export/index.d.ts +1 -6
- package/dist/board/export/index.js +16 -10
- package/dist/board/export/node/index.d.ts +2 -7
- package/dist/board/export/node/index.js +2 -5
- package/dist/board/export/scene.d.ts +3 -7
- package/dist/board/export/scene.js +22 -26
- package/dist/board/model/asset-key.d.ts +5 -2
- package/dist/board/model/asset-key.js +13 -5
- package/dist/board/model/index.d.ts +5 -4
- package/dist/board/model/index.js +4 -3
- package/dist/board/model/media-playback.d.ts +4 -6
- package/dist/board/model/media-playback.js +4 -4
- package/dist/board/model/read.d.ts +7 -0
- package/dist/board/model/read.js +18 -0
- package/dist/board/model/themes.d.ts +50 -0
- package/dist/board/model/themes.js +44 -6
- package/dist/board/render/backdrop-layer.d.ts +21 -0
- package/dist/board/render/backdrop-layer.js +227 -0
- package/dist/board/render/backdrop.d.ts +45 -0
- package/dist/board/render/backdrop.js +62 -0
- package/dist/board/render/index.d.ts +3 -2
- package/dist/board/render/index.js +7 -6
- package/dist/board/render/renderers/arrow-card-renderer.js +1 -1
- package/dist/board/render/renderers/audio-card-renderer.js +2 -2
- package/dist/board/render/renderers/file-card-renderer.js +1 -1
- package/dist/board/render/renderers/frame-card-renderer.js +1 -1
- package/dist/board/render/renderers/model-card-renderer.js +1 -1
- package/dist/board/render/renderers/shape-card-renderer.js +1 -1
- package/dist/board/render/renderers/task-card-renderer.js +2 -2
- package/dist/board/render/renderers/text-card-renderer.js +2 -2
- package/dist/board/render/renderers/video-card-renderer.js +1 -1
- package/dist/board/render/theme.d.ts +2 -0
- package/dist/board/render/theme.js +4 -2
- package/dist/board/replica/index.d.ts +3 -2
- package/dist/board/replica/index.js +34 -24
- package/dist/board/replica/indexeddb.js +14 -85
- package/dist/board/stage/assets.d.ts +4 -3
- package/dist/board/stage/assets.js +186 -83
- package/dist/board/stage/backdrop.d.ts +25 -0
- package/dist/board/stage/backdrop.js +104 -0
- package/dist/board/stage/index.d.ts +2 -1
- package/dist/board/stage/scene.js +12 -5
- package/dist/board/stage/stage.d.ts +7 -2
- package/dist/board/stage/stage.js +44 -24
- package/dist/board/stage/texture-pool.d.ts +1 -1
- package/dist/board/stage/texture-pool.js +4 -5
- package/dist/chunks/app-realtime.js +1 -1
- package/dist/chunks/environment.d.ts +34 -21
- package/dist/chunks/environment.js +4 -3
- package/dist/chunks/http-error.js +28 -5
- package/dist/chunks/http.d.ts +87 -18
- package/dist/chunks/http.js +1855 -59
- package/dist/chunks/websocket.d.ts +9 -1
- package/dist/http.d.ts +2 -2
- package/dist/http.js +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +6 -1080
- package/dist/indexeddb.js +79 -0
- package/dist/media.d.ts +18 -32
- package/dist/media.js +81 -67
- package/dist/protocol/dist/board-model.d.ts +33 -26
- package/dist/protocol/dist/board-model.js +21 -18
- package/dist/protocol/dist/board.d.ts +21 -1
- package/dist/protocol/dist/board.js +5 -1
- package/dist/protocol/dist/core/media.d.ts +32 -9
- package/dist/protocol/dist/core/media.js +203 -48
- package/dist/protocol/dist/index.d.ts +3 -3
- package/dist/voice-input.js +1 -1
- package/dist/websocket.js +37 -5
- package/package.json +1 -1
- package/dist/board/render/board-background.d.ts +0 -18
- package/dist/board/render/board-background.js +0 -104
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { MEDIA_VARIANT_SIZES, fitsVideoFrames, imageVariantUrl, mediaHost, mediaVariantSize, variantSource, videoFrameUrl } from "../../protocol/dist/core/media.js";
|
|
2
|
+
import { loadMediaInfo, loadMediaSize } from "../../media.js";
|
|
3
3
|
import { boardModelFormat } from "../model/media.js";
|
|
4
|
-
import { boardAssetKey, boardAssetKeySource, boardAssetRefKey } from "../model/asset-key.js";
|
|
4
|
+
import { boardAssetKey, boardAssetKeySource, boardAssetRefKey, boardMediaNaturalSize } from "../model/asset-key.js";
|
|
5
5
|
import { loadVideoThumbnailTexture, videoTextureNaturalSize } from "../render/video-thumbnail.js";
|
|
6
6
|
import { planLruTrim } from "./lru.js";
|
|
7
7
|
import { defaultBoardDeviceBudget } from "./device.js";
|
|
@@ -9,12 +9,43 @@ import { scheduleBoardIdle } from "./idle.js";
|
|
|
9
9
|
import { boardImageTexturePool, loadBoardImageTexture, loadFittedImageTexture, readTextureTone, shrinkBoardTexture, unloadBoardImageTexture } from "./texture-pool.js";
|
|
10
10
|
import { Texture } from "pixi.js";
|
|
11
11
|
//#region src/board/stage/assets.ts
|
|
12
|
+
const SMALLEST_VARIANT = MEDIA_VARIANT_SIZES[0];
|
|
12
13
|
function variantUrl(source, edge, cover) {
|
|
13
14
|
if (!(edge <= 1536)) return null;
|
|
14
15
|
const url = imageVariantUrl(source, edge, cover ? "cover" : "contain");
|
|
15
16
|
return url && url !== source ? url : null;
|
|
16
17
|
}
|
|
17
|
-
const
|
|
18
|
+
const frameAspects = /* @__PURE__ */ new Map();
|
|
19
|
+
const FRAME_ASPECT_LIMIT = 2048;
|
|
20
|
+
const FRAME_SLACK = 1.25;
|
|
21
|
+
function learnAspect(source, { width, height }) {
|
|
22
|
+
if (!width || !height || frameAspects.has(source)) return;
|
|
23
|
+
frameAspects.set(source, width / height);
|
|
24
|
+
if (frameAspects.size > FRAME_ASPECT_LIMIT) frameAspects.delete(frameAspects.keys().next().value);
|
|
25
|
+
}
|
|
26
|
+
const FRAME_FACTS_WAIT_MS = 300;
|
|
27
|
+
async function factSize(source) {
|
|
28
|
+
const timeout = new Promise((resolve) => setTimeout(resolve, FRAME_FACTS_WAIT_MS, null));
|
|
29
|
+
const info = await Promise.race([loadMediaInfo(source, "video"), timeout]);
|
|
30
|
+
return info?.width && info.height ? {
|
|
31
|
+
width: info.width,
|
|
32
|
+
height: info.height
|
|
33
|
+
} : void 0;
|
|
34
|
+
}
|
|
35
|
+
function frameUrl(source, edge, natural) {
|
|
36
|
+
return videoFrameUrl(source, mediaVariantSize(edge), {
|
|
37
|
+
natural,
|
|
38
|
+
aspect: natural ? null : frameAspects.get(source)
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function textureMedia(media, url) {
|
|
42
|
+
return media === "video" && url && variantSource(url)?.type === "video" ? "image" : media;
|
|
43
|
+
}
|
|
44
|
+
function slotOf(media, source, frameless = false) {
|
|
45
|
+
return media === "video" && !frameless && videoFrameUrl(source, SMALLEST_VARIANT) ? "image" : media;
|
|
46
|
+
}
|
|
47
|
+
const FRAMELESS = /* @__PURE__ */ new Error("Video frame failed");
|
|
48
|
+
const NO_PREVIEW = /* @__PURE__ */ new Error("No preview without decoding the video");
|
|
18
49
|
function boundEdge({ width, height }, cover) {
|
|
19
50
|
return cover ? Math.min(width, height) : Math.max(width, height);
|
|
20
51
|
}
|
|
@@ -62,7 +93,8 @@ function createBoardAssetManager(options) {
|
|
|
62
93
|
}
|
|
63
94
|
await unloadBoardImageTexture(url, texture);
|
|
64
95
|
});
|
|
65
|
-
function releaseTexture(
|
|
96
|
+
function releaseTexture(asset, url, texture) {
|
|
97
|
+
const media = textureMedia(asset, url);
|
|
66
98
|
const pendingKey = `${media}:${url}`;
|
|
67
99
|
const promise = unloadTexture(url, texture, media).catch(() => {}).then(() => {
|
|
68
100
|
if (url && pendingUnloads.get(pendingKey) === promise) pendingUnloads.delete(pendingKey);
|
|
@@ -78,11 +110,11 @@ function createBoardAssetManager(options) {
|
|
|
78
110
|
if (typeof requestAnimationFrame === "function") requestAnimationFrame(release);
|
|
79
111
|
else setTimeout(release, 0);
|
|
80
112
|
}
|
|
81
|
-
const resolveFileUrl = options.resolveFileUrl;
|
|
82
113
|
const entries = /* @__PURE__ */ new Map();
|
|
114
|
+
const keysByPath = /* @__PURE__ */ new Map();
|
|
83
115
|
const pathVersions = /* @__PURE__ */ new Map();
|
|
84
116
|
const starters = /* @__PURE__ */ new Map();
|
|
85
|
-
const
|
|
117
|
+
const unavailable = /* @__PURE__ */ new Set();
|
|
86
118
|
const queue = [];
|
|
87
119
|
const listeners = /* @__PURE__ */ new Set();
|
|
88
120
|
let active = 0;
|
|
@@ -92,6 +124,7 @@ function createBoardAssetManager(options) {
|
|
|
92
124
|
model: 0
|
|
93
125
|
};
|
|
94
126
|
let disposed = false;
|
|
127
|
+
let stopSource = null;
|
|
95
128
|
let focus = null;
|
|
96
129
|
let trimTimer = null;
|
|
97
130
|
let trimSince = 0;
|
|
@@ -131,27 +164,52 @@ function createBoardAssetManager(options) {
|
|
|
131
164
|
cancelTone = toneQueue.size > 0 ? scheduleBoardIdle(readNextTone, 0) : null;
|
|
132
165
|
}
|
|
133
166
|
const keys = /* @__PURE__ */ new WeakMap();
|
|
167
|
+
const itemEpochs = /* @__PURE__ */ new Map();
|
|
134
168
|
let keyEpoch = 0;
|
|
135
169
|
function managedAssetKey(item) {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
170
|
+
const itemEpoch = itemEpochs.get(item.id) ?? 0;
|
|
171
|
+
let record = keys.get(item);
|
|
172
|
+
if (record?.epoch !== keyEpoch || record.itemEpoch !== itemEpoch) {
|
|
173
|
+
const { key: base, natural } = resolveAsset(item);
|
|
174
|
+
const source = base ? boardAssetKeySource(base) : null;
|
|
175
|
+
const path = source?.kind === "file" ? source.value : null;
|
|
176
|
+
record = {
|
|
177
|
+
epoch: keyEpoch,
|
|
178
|
+
itemEpoch,
|
|
179
|
+
base,
|
|
180
|
+
path,
|
|
181
|
+
version: 0,
|
|
182
|
+
key: base,
|
|
183
|
+
natural
|
|
184
|
+
};
|
|
185
|
+
keys.set(item, record);
|
|
186
|
+
}
|
|
187
|
+
if (record.path !== null) {
|
|
188
|
+
const version = pathVersions.get(record.path) ?? 0;
|
|
189
|
+
if (version !== record.version) {
|
|
190
|
+
record.version = version;
|
|
191
|
+
record.key = `local:${version}:${record.base}`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return record.key;
|
|
144
195
|
}
|
|
145
|
-
function
|
|
196
|
+
function resolveAsset(item) {
|
|
146
197
|
const asset = options.itemAsset?.(item);
|
|
198
|
+
const natural = (asset === void 0 ? boardMediaNaturalSize(item) : asset?.natural) ?? null;
|
|
147
199
|
const key = asset === void 0 ? boardAssetKey(item) : asset && boardAssetRefKey(asset);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
200
|
+
const source = key ? boardAssetKeySource(key) : null;
|
|
201
|
+
if (source?.media === "video" && source.kind === "url" && options.loadVideoPreviews === false && slotOf("video", source.value) === "video") return {
|
|
202
|
+
key: null,
|
|
203
|
+
natural
|
|
204
|
+
};
|
|
205
|
+
if (source?.media === "model" && !modelPreviewable(item)) return {
|
|
206
|
+
key: null,
|
|
207
|
+
natural
|
|
208
|
+
};
|
|
209
|
+
return {
|
|
210
|
+
key,
|
|
211
|
+
natural
|
|
212
|
+
};
|
|
155
213
|
}
|
|
156
214
|
function clearRetry(entry) {
|
|
157
215
|
if (entry.retryTimer) {
|
|
@@ -163,9 +221,11 @@ function createBoardAssetManager(options) {
|
|
|
163
221
|
let entry = entries.get(key);
|
|
164
222
|
if (!entry) {
|
|
165
223
|
const source = boardAssetKeySource(key);
|
|
224
|
+
const path = source?.kind === "file" ? source.value : null;
|
|
166
225
|
entry = {
|
|
167
226
|
media: source?.media ?? "image",
|
|
168
227
|
cover: source?.fit === "cover",
|
|
228
|
+
path,
|
|
169
229
|
source: null,
|
|
170
230
|
url: null,
|
|
171
231
|
texture: null,
|
|
@@ -177,11 +237,17 @@ function createBoardAssetManager(options) {
|
|
|
177
237
|
loading: false,
|
|
178
238
|
error: false,
|
|
179
239
|
attempts: 0,
|
|
240
|
+
frameless: false,
|
|
180
241
|
retryAt: 0,
|
|
181
242
|
retryTimer: null,
|
|
182
243
|
lastUsedAt: now()
|
|
183
244
|
};
|
|
184
245
|
entries.set(key, entry);
|
|
246
|
+
if (path !== null) {
|
|
247
|
+
const keyed = keysByPath.get(path);
|
|
248
|
+
if (keyed) keyed.add(key);
|
|
249
|
+
else keysByPath.set(path, /* @__PURE__ */ new Set([key]));
|
|
250
|
+
}
|
|
185
251
|
}
|
|
186
252
|
return entry;
|
|
187
253
|
}
|
|
@@ -203,6 +269,10 @@ function createBoardAssetManager(options) {
|
|
|
203
269
|
starters.delete(key);
|
|
204
270
|
if (entry.url || entry.texture) releaseTexture(entry.media, entry.url ?? "", entry.texture);
|
|
205
271
|
entries.delete(key);
|
|
272
|
+
if (entry.path === null) return;
|
|
273
|
+
const keyed = keysByPath.get(entry.path);
|
|
274
|
+
keyed?.delete(key);
|
|
275
|
+
if (keyed?.size === 0) keysByPath.delete(entry.path);
|
|
206
276
|
}
|
|
207
277
|
function scheduleTrim() {
|
|
208
278
|
const at = now();
|
|
@@ -214,8 +284,8 @@ function createBoardAssetManager(options) {
|
|
|
214
284
|
if (!disposed) trim();
|
|
215
285
|
}, TRIM_DELAY);
|
|
216
286
|
}
|
|
217
|
-
function shrunkFootprint({ media, texture }) {
|
|
218
|
-
if (!shrinkTexture || media !== "image" || !texture) return void 0;
|
|
287
|
+
function shrunkFootprint({ media, texture, url }) {
|
|
288
|
+
if (!shrinkTexture || textureMedia(media, url ?? "") !== "image" || !texture) return void 0;
|
|
219
289
|
const scale = SHRINK_EDGE / Math.max(texture.width, texture.height);
|
|
220
290
|
return scale < 1 ? Math.round(texture.width * scale) * Math.round(texture.height * scale) * 4 : void 0;
|
|
221
291
|
}
|
|
@@ -275,7 +345,9 @@ function createBoardAssetManager(options) {
|
|
|
275
345
|
entry.texture = texture;
|
|
276
346
|
entry.url = loaded.url;
|
|
277
347
|
entry.edge = loaded.edge;
|
|
278
|
-
|
|
348
|
+
const decoded = videoTextureNaturalSize(texture);
|
|
349
|
+
if (decoded) entry.natural = decoded;
|
|
350
|
+
else if (loaded.edge === Number.POSITIVE_INFINITY && !variantSource(loaded.url)) entry.natural = {
|
|
279
351
|
width: texture.width,
|
|
280
352
|
height: texture.height
|
|
281
353
|
};
|
|
@@ -292,23 +364,27 @@ function createBoardAssetManager(options) {
|
|
|
292
364
|
notify(key);
|
|
293
365
|
return;
|
|
294
366
|
}
|
|
295
|
-
|
|
367
|
+
if (error === FRAMELESS && entry.source && entry.refs > 0) return enqueue(key, "video", textureLoad("video", entry.source));
|
|
368
|
+
const final = entry.media === "model" || error === NO_PREVIEW;
|
|
296
369
|
entry.error = true;
|
|
297
370
|
entry.attempts += 1;
|
|
298
371
|
entry.retryAt = now() + Math.min(1e3 * 2 ** entry.attempts, MAX_RETRY_DELAY);
|
|
299
|
-
console.warn(`[board] failed to load preview for ${key}`, error);
|
|
300
|
-
if (
|
|
372
|
+
if (error !== NO_PREVIEW) console.warn(`[board] failed to load preview for ${key}`, error);
|
|
373
|
+
if (final) unavailable.add(key);
|
|
301
374
|
if (entry.refs <= 0) evict(key, entry);
|
|
302
375
|
else {
|
|
303
|
-
if (
|
|
376
|
+
if (!final) scheduleRetry(key, entry);
|
|
304
377
|
notify(key);
|
|
305
378
|
}
|
|
306
379
|
}
|
|
307
380
|
function refinable(key, entry) {
|
|
308
|
-
|
|
381
|
+
if (entry.media === "model" || !(entry.refs > 0 || warm.has(key)) || !entry.texture || !entry.source || entry.want <= entry.edge) return false;
|
|
382
|
+
return entry.media === "image" || frameUrl(entry.source, entry.want, entry.natural) !== entry.url;
|
|
309
383
|
}
|
|
310
384
|
function refine(key, entry) {
|
|
311
|
-
if (
|
|
385
|
+
if (entry.loading || !entry.source || entry.media === "model") return;
|
|
386
|
+
if (refinable(key, entry)) enqueue(key, slotOf(entry.media, entry.source, entry.frameless), textureLoad(entry.media, entry.source));
|
|
387
|
+
else if (entry.media === "video" && entry.natural === void 0 && entry.texture && boundEdge(entry.texture, false) < entry.want && !fitsVideoFrames(entry.source)) probeNaturalSize(key, entry, entry.source);
|
|
312
388
|
}
|
|
313
389
|
function enqueue(key, media, load) {
|
|
314
390
|
if (queue.some((task) => task.key === key)) return;
|
|
@@ -362,55 +438,79 @@ function createBoardAssetManager(options) {
|
|
|
362
438
|
task.load(entry).then((loaded) => done(loaded), (error) => done(null, error));
|
|
363
439
|
}
|
|
364
440
|
}
|
|
441
|
+
async function loadSized(entry, url, media, resized, slack = 2) {
|
|
442
|
+
const { cover, want: edge } = entry;
|
|
443
|
+
const pending = pendingUnloads.get(`${media}:${url}`);
|
|
444
|
+
if (pending) await pending;
|
|
445
|
+
if (disposed) return null;
|
|
446
|
+
const texture = await loadTexture(url, media);
|
|
447
|
+
if (!texture) return null;
|
|
448
|
+
const bound = boundEdge(texture, cover);
|
|
449
|
+
if (media === "image" && shrinkTexture && edge < bound / slack) {
|
|
450
|
+
const longEdge = Math.round(edge * Math.max(texture.width, texture.height) / bound);
|
|
451
|
+
const small = await shrinkTexture(texture, longEdge).catch(() => null);
|
|
452
|
+
if (small) {
|
|
453
|
+
releaseTexture(media, url, texture);
|
|
454
|
+
return {
|
|
455
|
+
texture: small,
|
|
456
|
+
url: "",
|
|
457
|
+
edge
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
texture,
|
|
463
|
+
url,
|
|
464
|
+
edge: sharpEdge(texture, edge, resized, cover)
|
|
465
|
+
};
|
|
466
|
+
}
|
|
365
467
|
function textureLoad(media, source) {
|
|
366
468
|
return async (entry) => {
|
|
367
469
|
entry.source = source;
|
|
368
470
|
const { cover, want: edge } = entry;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
471
|
+
if (media === "video") {
|
|
472
|
+
if (entry.natural === void 0 && !entry.frameless && mediaHost(source)?.metaHeaders) entry.natural = await factSize(source);
|
|
473
|
+
const frame = entry.frameless ? null : frameUrl(source, edge, entry.natural);
|
|
474
|
+
if (frame) {
|
|
475
|
+
const loaded = await loadSized(entry, frame, "image", true, FRAME_SLACK).catch(() => null);
|
|
476
|
+
if (loaded) {
|
|
477
|
+
learnAspect(source, loaded.texture);
|
|
478
|
+
if (!entry.natural && !fitsVideoFrames(source) && loaded.edge === Number.POSITIVE_INFINITY) loaded.edge = boundEdge(loaded.texture, false);
|
|
479
|
+
}
|
|
480
|
+
if (loaded || entry.texture) return loaded;
|
|
481
|
+
entry.frameless = true;
|
|
482
|
+
throw FRAMELESS;
|
|
483
|
+
}
|
|
484
|
+
if (options.loadVideoPreviews === false) throw NO_PREVIEW;
|
|
485
|
+
return loadSized(entry, source, "video", false);
|
|
486
|
+
}
|
|
487
|
+
const variant = variantUrl(source, Math.max(edge, SMALLEST_VARIANT), cover);
|
|
488
|
+
if (!variant && useSharedImagePool && Number.isFinite(edge)) {
|
|
489
|
+
const fitted = await loadFittedImageTexture(source, edge, cover).catch(() => null);
|
|
376
490
|
if (fitted) return {
|
|
377
491
|
texture: fitted,
|
|
378
492
|
url: "",
|
|
379
493
|
edge: sharpEdge(fitted, edge, true, cover)
|
|
380
494
|
};
|
|
381
495
|
}
|
|
382
|
-
|
|
383
|
-
if (!texture) return null;
|
|
384
|
-
const bound = boundEdge(texture, cover);
|
|
385
|
-
if (media === "image" && shrinkTexture && edge < bound / 2) {
|
|
386
|
-
const longEdge = Math.round(edge * Math.max(texture.width, texture.height) / bound);
|
|
387
|
-
const small = await shrinkTexture(texture, longEdge).catch(() => null);
|
|
388
|
-
if (small) {
|
|
389
|
-
releaseTexture(media, url, texture);
|
|
390
|
-
return {
|
|
391
|
-
texture: small,
|
|
392
|
-
url: "",
|
|
393
|
-
edge
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
return {
|
|
398
|
-
texture,
|
|
399
|
-
url,
|
|
400
|
-
edge: sharpEdge(texture, edge, Boolean(variant), cover)
|
|
401
|
-
};
|
|
496
|
+
return loadSized(entry, variant ?? source, "image", Boolean(variant));
|
|
402
497
|
};
|
|
403
498
|
}
|
|
404
|
-
function
|
|
499
|
+
function startFile(key, media, path) {
|
|
405
500
|
return (entry) => {
|
|
406
501
|
const fresh = entry.attempts > 0;
|
|
407
502
|
entry.loading = true;
|
|
408
|
-
|
|
503
|
+
options.resolveFile(path, fresh ? { fresh } : void 0).then((file) => {
|
|
409
504
|
if (disposed || entries.get(key) !== entry) return;
|
|
410
505
|
if (entry.refs <= 0 && !warm.has(key)) return evict(key, entry);
|
|
411
|
-
if (!
|
|
506
|
+
if (!file) return adopt(key, entry, null, /* @__PURE__ */ new Error(`Unavailable file: ${path}`));
|
|
507
|
+
const { url, media: facts } = file;
|
|
508
|
+
if (facts && !entry.natural) entry.natural = {
|
|
509
|
+
width: facts.width,
|
|
510
|
+
height: facts.height
|
|
511
|
+
};
|
|
412
512
|
entry.loading = false;
|
|
413
|
-
enqueue(key, media, textureLoad(media, url));
|
|
513
|
+
enqueue(key, slotOf(media, url, entry.frameless), textureLoad(media, url));
|
|
414
514
|
}, (error) => {
|
|
415
515
|
if (!disposed) adopt(key, entry, null, error);
|
|
416
516
|
});
|
|
@@ -418,12 +518,10 @@ function createBoardAssetManager(options) {
|
|
|
418
518
|
}
|
|
419
519
|
function probeNaturalSize(key, entry, source) {
|
|
420
520
|
entry.natural = null;
|
|
421
|
-
|
|
422
|
-
if (disposed || entries.get(key) !== entry || !
|
|
423
|
-
entry.natural =
|
|
424
|
-
|
|
425
|
-
height
|
|
426
|
-
};
|
|
521
|
+
loadMediaSize(source, entry.media === "video" ? "video" : "image").then((size) => {
|
|
522
|
+
if (disposed || entries.get(key) !== entry || !size) return;
|
|
523
|
+
entry.natural = size;
|
|
524
|
+
refine(key, entry);
|
|
427
525
|
notify(key);
|
|
428
526
|
});
|
|
429
527
|
}
|
|
@@ -457,34 +555,36 @@ function createBoardAssetManager(options) {
|
|
|
457
555
|
return load ? () => enqueue(key, "model", load) : null;
|
|
458
556
|
}
|
|
459
557
|
const media = source.media;
|
|
460
|
-
if (source.kind === "file") return
|
|
558
|
+
if (source.kind === "file") return startFile(key, media, source.value);
|
|
461
559
|
const load = textureLoad(media, source.value);
|
|
462
|
-
return () => enqueue(key, media, load);
|
|
560
|
+
return (entry) => enqueue(key, slotOf(media, source.value, entry.frameless), load);
|
|
463
561
|
}
|
|
464
562
|
const manager = {
|
|
465
563
|
assetKey: managedAssetKey,
|
|
466
564
|
invalidatePath(path) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const source = boardAssetKeySource(key);
|
|
470
|
-
return source?.kind === "file" && source.value === path;
|
|
471
|
-
});
|
|
472
|
-
if (stale.length === 0) return;
|
|
565
|
+
const stale = keysByPath.get(path);
|
|
566
|
+
if (disposed || !stale) return;
|
|
473
567
|
pathVersions.set(path, (pathVersions.get(path) ?? 0) + 1);
|
|
474
|
-
|
|
475
|
-
for (const key of stale) notify(key);
|
|
568
|
+
for (const key of [...stale]) notify(key);
|
|
476
569
|
},
|
|
477
|
-
invalidateItemAssets() {
|
|
478
|
-
if (options.itemAsset)
|
|
570
|
+
invalidateItemAssets(ids) {
|
|
571
|
+
if (!options.itemAsset) return;
|
|
572
|
+
if (!ids) {
|
|
573
|
+
keyEpoch += 1;
|
|
574
|
+
itemEpochs.clear();
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
for (const id of ids) itemEpochs.set(id, (itemEpochs.get(id) ?? 0) + 1);
|
|
479
578
|
},
|
|
480
579
|
requestItem(item, edge = Number.POSITIVE_INFINITY) {
|
|
481
580
|
if (disposed) return;
|
|
482
581
|
const key = managedAssetKey(item);
|
|
483
|
-
if (!key ||
|
|
582
|
+
if (!key || unavailable.has(key)) return;
|
|
484
583
|
const source = boardAssetKeySource(key);
|
|
485
584
|
if (!source) return;
|
|
486
585
|
const entry = ensureEntry(key);
|
|
487
586
|
entry.lastUsedAt = now();
|
|
587
|
+
if (!entry.natural) entry.natural = keys.get(item)?.natural ?? entry.natural;
|
|
488
588
|
entry.center = {
|
|
489
589
|
x: item.frame.x + item.frame.width / 2,
|
|
490
590
|
y: item.frame.y + item.frame.height / 2
|
|
@@ -659,6 +759,7 @@ function createBoardAssetManager(options) {
|
|
|
659
759
|
},
|
|
660
760
|
destroy() {
|
|
661
761
|
disposed = true;
|
|
762
|
+
stopSource?.();
|
|
662
763
|
if (trimTimer !== null) clearTimeout(trimTimer);
|
|
663
764
|
trimTimer = null;
|
|
664
765
|
cancelTone?.();
|
|
@@ -669,13 +770,15 @@ function createBoardAssetManager(options) {
|
|
|
669
770
|
unwarmable.clear();
|
|
670
771
|
listeners.clear();
|
|
671
772
|
pathVersions.clear();
|
|
773
|
+
itemEpochs.clear();
|
|
672
774
|
starters.clear();
|
|
673
|
-
|
|
775
|
+
unavailable.clear();
|
|
674
776
|
for (const [key, entry] of entries) evict(key, entry);
|
|
675
777
|
entries.clear();
|
|
676
778
|
for (const release of [...retiring]) release();
|
|
677
779
|
}
|
|
678
780
|
};
|
|
781
|
+
stopSource = options.subscribe?.((path) => manager.invalidatePath(path)) ?? null;
|
|
679
782
|
return manager;
|
|
680
783
|
}
|
|
681
784
|
//#endregion
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { BoardAssetSource } from "../model/media-playback.js";
|
|
2
|
+
import "../model/index.js";
|
|
3
|
+
import { BoardBackdrop } from "../render/backdrop.js";
|
|
4
|
+
import { BoardBackdropView } from "../render/backdrop-layer.js";
|
|
5
|
+
import "../render/index.js";
|
|
6
|
+
import { BoardImageTexturePool } from "./texture-pool.js";
|
|
7
|
+
import { Renderer } from "pixi.js";
|
|
8
|
+
//#region src/board/stage/backdrop.d.ts
|
|
9
|
+
type BoardBackdropStatus = {
|
|
10
|
+
src: string;
|
|
11
|
+
status: "loading" | "ready" | "error";
|
|
12
|
+
};
|
|
13
|
+
declare function createBoardStageBackdrop(options: {
|
|
14
|
+
renderer: Renderer;
|
|
15
|
+
source: BoardAssetSource;
|
|
16
|
+
pool?: BoardImageTexturePool;
|
|
17
|
+
requestFrame: () => void;
|
|
18
|
+
onStatus?: (status: BoardBackdropStatus | null) => void;
|
|
19
|
+
}): {
|
|
20
|
+
container: import("pixi.js").Container<import("pixi.js").ContainerChild>;
|
|
21
|
+
sync(backdrop: BoardBackdrop, view: BoardBackdropView, reducedMotion: boolean): void;
|
|
22
|
+
destroy(): void;
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { BoardBackdropStatus, createBoardStageBackdrop };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { imageVariantUrl, mediaVariantSize } from "../../protocol/dist/core/media.js";
|
|
2
|
+
import { parseBoardMediaSource } from "../../protocol/dist/board-model.js";
|
|
3
|
+
import { createBoardBackdropLayer } from "../render/backdrop-layer.js";
|
|
4
|
+
import { boardImageTexturePool } from "./texture-pool.js";
|
|
5
|
+
import { RendererType } from "pixi.js";
|
|
6
|
+
//#region src/board/stage/backdrop.ts
|
|
7
|
+
const FADE_MS = 160;
|
|
8
|
+
const RETRY_MIN_MS = 2e3;
|
|
9
|
+
const RETRY_MAX_MS = 32e3;
|
|
10
|
+
function createBoardStageBackdrop(options) {
|
|
11
|
+
const layer = createBoardBackdropLayer({ patterns: options.renderer.type === RendererType.WEBGL });
|
|
12
|
+
const pool = options.pool ?? boardImageTexturePool;
|
|
13
|
+
let loadKey = null;
|
|
14
|
+
let file = null;
|
|
15
|
+
let held = null;
|
|
16
|
+
let texture = null;
|
|
17
|
+
let readyAt = 0;
|
|
18
|
+
let generation = 0;
|
|
19
|
+
let retry = null;
|
|
20
|
+
const reload = () => {
|
|
21
|
+
loadKey = null;
|
|
22
|
+
options.requestFrame();
|
|
23
|
+
};
|
|
24
|
+
const stopFiles = options.source.subscribe?.((path) => {
|
|
25
|
+
if (path === file) reload();
|
|
26
|
+
});
|
|
27
|
+
function retryLater(key) {
|
|
28
|
+
const delay = retry?.key === key ? retry.delay * 2 : RETRY_MIN_MS;
|
|
29
|
+
stopRetry();
|
|
30
|
+
if (delay <= RETRY_MAX_MS) retry = {
|
|
31
|
+
key,
|
|
32
|
+
delay,
|
|
33
|
+
timer: setTimeout(reload, delay)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function stopRetry() {
|
|
37
|
+
if (retry) clearTimeout(retry.timer);
|
|
38
|
+
retry = null;
|
|
39
|
+
}
|
|
40
|
+
function show(key, next) {
|
|
41
|
+
if (held) pool.release(held);
|
|
42
|
+
if (next && !texture) readyAt = performance.now();
|
|
43
|
+
held = key;
|
|
44
|
+
texture = next;
|
|
45
|
+
}
|
|
46
|
+
function edgeFor(image, view) {
|
|
47
|
+
if (image.fit !== "repeat") return Math.max(view.width, view.height) * view.density;
|
|
48
|
+
return image.tile ? Math.max(image.tile.width, image.tile.height) * view.density : Number.POSITIVE_INFINITY;
|
|
49
|
+
}
|
|
50
|
+
async function load(image, edge, current, attempt) {
|
|
51
|
+
const report = (status) => {
|
|
52
|
+
if (current === generation) options.onStatus?.({
|
|
53
|
+
src: image.src,
|
|
54
|
+
status
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
report("loading");
|
|
58
|
+
const source = parseBoardMediaSource(image.src);
|
|
59
|
+
file = source?.kind === "file" ? source.value : null;
|
|
60
|
+
const url = file ? await options.source.resolveFile(file).then((resolved) => resolved?.url ?? null, () => null) : image.src;
|
|
61
|
+
if (current !== generation) return;
|
|
62
|
+
const fit = image.fit === "cover" ? "cover" : "contain";
|
|
63
|
+
const key = url && (edge <= 1536 && imageVariantUrl(url, mediaVariantSize(edge), fit) || url);
|
|
64
|
+
const loaded = key ? await pool.acquire(key).catch(() => null) : null;
|
|
65
|
+
if (current !== generation) {
|
|
66
|
+
if (key && loaded) pool.release(key);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
show(loaded ? key : null, loaded);
|
|
70
|
+
report(loaded ? "ready" : "error");
|
|
71
|
+
if (loaded) stopRetry();
|
|
72
|
+
else retryLater(attempt);
|
|
73
|
+
options.requestFrame();
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
container: layer.container,
|
|
77
|
+
sync(backdrop, view, reducedMotion) {
|
|
78
|
+
const next = backdrop.image ? `${backdrop.image.fit} ${backdrop.image.src}` : null;
|
|
79
|
+
if (next !== loadKey) {
|
|
80
|
+
loadKey = next;
|
|
81
|
+
generation += 1;
|
|
82
|
+
if (retry && retry.key !== next) stopRetry();
|
|
83
|
+
if (backdrop.image && next) load(backdrop.image, edgeFor(backdrop.image, view), generation, next);
|
|
84
|
+
else {
|
|
85
|
+
file = null;
|
|
86
|
+
show(null, null);
|
|
87
|
+
options.onStatus?.(null);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const fade = reducedMotion ? 1 : Math.min(1, (performance.now() - readyAt) / FADE_MS);
|
|
91
|
+
layer.sync(backdrop, view, texture, fade);
|
|
92
|
+
if (texture && fade < 1) options.requestFrame();
|
|
93
|
+
},
|
|
94
|
+
destroy() {
|
|
95
|
+
generation += 1;
|
|
96
|
+
stopRetry();
|
|
97
|
+
stopFiles?.();
|
|
98
|
+
show(null, null);
|
|
99
|
+
layer.destroy();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
export { createBoardStageBackdrop };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, LruTrim, planLruTrim, selectLruEvictions } from "./lru.js";
|
|
2
2
|
import { BoardImageTexturePool, boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, loadFittedImageTexture, readTextureTone, shrinkBoardTexture, unloadBoardImageTexture } from "./texture-pool.js";
|
|
3
3
|
import { BoardAssetChange, BoardAssetManager, BoardAssetManagerOptions, BoardAssetPrefetch, BoardModelPreviewRequest, BoardModelPreviews, createBoardAssetManager } from "./assets.js";
|
|
4
|
+
import { BoardBackdropStatus } from "./backdrop.js";
|
|
4
5
|
import { BOARD_LONG_PRESS_MS, BoardStagePress } from "./presses.js";
|
|
5
6
|
import { BoardFrameBudget, BoardFrameWork } from "./frame-budget.js";
|
|
6
7
|
import { BoardLiveStroke, BoardScene, BoardSceneNode, SceneSyncInput, createBoardScene } from "./scene.js";
|
|
@@ -11,4 +12,4 @@ import { BOARD_DEVICE_TIERS, BoardDeviceBudget, BoardDeviceEnvironment, BoardDev
|
|
|
11
12
|
import { toBoardPointerEvent } from "./pointer.js";
|
|
12
13
|
import { createBoardPointerGestures } from "./pointer-gestures.js";
|
|
13
14
|
import { BoardOverlayInput, drawBoardOverlay, expandFrame } from "./overlay.js";
|
|
14
|
-
export { BOARD_BENCH_PHASES, BOARD_DEVICE_TIERS, BOARD_LONG_PRESS_MS, BoardAssetChange, BoardAssetManager, BoardAssetManagerOptions, BoardAssetPrefetch, BoardBenchPercentiles, BoardBenchPhase, BoardBenchPhaseReport, BoardBenchReport, BoardBenchSettle, BoardDeviceBudget, BoardDeviceEnvironment, BoardDeviceSignals, BoardDeviceTier, type BoardFrameBudget, type BoardFrameWork, BoardImageTexturePool, BoardItemView, BoardItemViewContext, BoardItemViewInstance, BoardItemViewLayer, BoardLiveStroke, BoardModelPreviewRequest, BoardModelPreviews, BoardOverlayInput, BoardScene, BoardSceneNode, BoardStage, BoardStageOptions, BoardStagePointerPhase, type BoardStagePress, BoardStageStats, BoardStageTheme, COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, LruTrim, SceneSyncInput, boardDeviceBudget, boardDeviceTier, boardImageTexturePool, createBoardAssetManager, createBoardImageTexturePool, createBoardItemViewLayer, createBoardPointerGestures, createBoardScene, defaultBoardDeviceBudget, drawBoardOverlay, expandFrame, loadBoardImageTexture, loadFittedImageTexture, mountBoardStage, planLruTrim, readBoardDeviceSignals, readTextureTone, runBoardStageBench, selectLruEvictions, shrinkBoardTexture, toBoardPointerEvent, unloadBoardImageTexture };
|
|
15
|
+
export { BOARD_BENCH_PHASES, BOARD_DEVICE_TIERS, BOARD_LONG_PRESS_MS, BoardAssetChange, BoardAssetManager, BoardAssetManagerOptions, BoardAssetPrefetch, type BoardBackdropStatus, BoardBenchPercentiles, BoardBenchPhase, BoardBenchPhaseReport, BoardBenchReport, BoardBenchSettle, BoardDeviceBudget, BoardDeviceEnvironment, BoardDeviceSignals, BoardDeviceTier, type BoardFrameBudget, type BoardFrameWork, BoardImageTexturePool, BoardItemView, BoardItemViewContext, BoardItemViewInstance, BoardItemViewLayer, BoardLiveStroke, BoardModelPreviewRequest, BoardModelPreviews, BoardOverlayInput, BoardScene, BoardSceneNode, BoardStage, BoardStageOptions, BoardStagePointerPhase, type BoardStagePress, BoardStageStats, BoardStageTheme, COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, LruTrim, SceneSyncInput, boardDeviceBudget, boardDeviceTier, boardImageTexturePool, createBoardAssetManager, createBoardImageTexturePool, createBoardItemViewLayer, createBoardPointerGestures, createBoardScene, defaultBoardDeviceBudget, drawBoardOverlay, expandFrame, loadBoardImageTexture, loadFittedImageTexture, mountBoardStage, planLruTrim, readBoardDeviceSignals, readTextureTone, runBoardStageBench, selectLruEvictions, shrinkBoardTexture, toBoardPointerEvent, unloadBoardImageTexture };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { parseBoardItem } from "../../protocol/dist/board-model.js";
|
|
2
2
|
import { drawPointsBounds } from "../../protocol/dist/board-layout.js";
|
|
3
3
|
import { itemBounds, rectsIntersect } from "../model/geometry.js";
|
|
4
|
-
import { DEFAULT_BOARD_LOD, boardCardDetailed, boardLodRatio, boardLodSize } from "../render/lod.js";
|
|
5
4
|
import { textZoomBucket } from "../render/text-resolution.js";
|
|
6
5
|
import { clippingAncestor, isClippingFrame, syncClipGroup } from "../render/clip.js";
|
|
6
|
+
import { DEFAULT_BOARD_LOD, boardCardDetailed, boardLodRatio, boardLodSize } from "../render/lod.js";
|
|
7
7
|
import { fixedBoardFrameBudget } from "./frame-budget.js";
|
|
8
8
|
import { Graphics, Text } from "pixi.js";
|
|
9
9
|
//#region src/board/stage/scene.ts
|
|
@@ -14,6 +14,7 @@ const Z_LIVE_STROKE = Number.MAX_SAFE_INTEGER - 1;
|
|
|
14
14
|
const Z_OVERLAY = Number.MAX_SAFE_INTEGER;
|
|
15
15
|
const POOL_LIMIT_PER_RENDERER = 48;
|
|
16
16
|
const EMPTY_IDS = /* @__PURE__ */ new Set();
|
|
17
|
+
const NO_PLATES = /* @__PURE__ */ new Map();
|
|
17
18
|
function idSetSignature(ids) {
|
|
18
19
|
let hash = ids.size;
|
|
19
20
|
for (const id of ids) {
|
|
@@ -257,13 +258,19 @@ function createBoardScene(options) {
|
|
|
257
258
|
let paintedPlan = null;
|
|
258
259
|
let paintedSig = "";
|
|
259
260
|
let platesPending = false;
|
|
260
|
-
let covered =
|
|
261
|
+
let covered = NO_PLATES;
|
|
261
262
|
/**
|
|
262
263
|
* Repaint the plates whose cards changed, within a per-frame budget. A card
|
|
263
264
|
* is covered once a plate draws it; until then it stays live if it already is.
|
|
264
265
|
*/
|
|
265
266
|
function paintPlates(input, current) {
|
|
266
267
|
const { context, globalSig, scene } = input;
|
|
268
|
+
for (const id of input.staleIds ?? EMPTY_IDS) {
|
|
269
|
+
const plate = covered.get(id);
|
|
270
|
+
if (!plate) continue;
|
|
271
|
+
plate.sig = "";
|
|
272
|
+
paintedPlan = null;
|
|
273
|
+
}
|
|
267
274
|
if (current === paintedPlan && globalSig === paintedSig) return false;
|
|
268
275
|
const start = performance.now();
|
|
269
276
|
const allowance = budget.allow("plate");
|
|
@@ -303,8 +310,8 @@ function createBoardScene(options) {
|
|
|
303
310
|
plate.graphics.destroy();
|
|
304
311
|
changed = true;
|
|
305
312
|
}
|
|
306
|
-
const next = /* @__PURE__ */ new
|
|
307
|
-
for (const plate of plates.values()) for (const item of plate.items) next.
|
|
313
|
+
const next = /* @__PURE__ */ new Map();
|
|
314
|
+
for (const plate of plates.values()) for (const item of plate.items) next.set(item.id, plate);
|
|
308
315
|
covered = next;
|
|
309
316
|
budget.spend("plate", painted, paintMs);
|
|
310
317
|
platesPending = pending;
|
|
@@ -328,7 +335,7 @@ function createBoardScene(options) {
|
|
|
328
335
|
paintedPlan = null;
|
|
329
336
|
paintedSig = "";
|
|
330
337
|
platesPending = false;
|
|
331
|
-
covered =
|
|
338
|
+
covered = NO_PLATES;
|
|
332
339
|
plan = null;
|
|
333
340
|
planInputs = [];
|
|
334
341
|
nearIds = EMPTY_IDS;
|