@neta-art/cohub 12.2.0 → 12.4.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 +87 -3
- package/dist/board/editor/context.d.ts +2 -0
- package/dist/board/editor/document.d.ts +3 -1
- package/dist/board/editor/document.js +8 -6
- package/dist/board/editor/editor.d.ts +7 -3
- package/dist/board/editor/editor.js +8 -1
- package/dist/board/editor/index.d.ts +3 -3
- package/dist/board/editor/index.js +2 -2
- package/dist/board/editor/interaction.d.ts +23 -2
- package/dist/board/editor/interaction.js +99 -5
- package/dist/board/editor/query.d.ts +1 -1
- package/dist/board/editor/query.js +3 -7
- package/dist/board/editor/selection.js +13 -1
- package/dist/board/editor/tool.d.ts +4 -1
- package/dist/board/editor/tool.js +6 -1
- package/dist/board/model/asset-key.d.ts +9 -1
- package/dist/board/model/asset-key.js +27 -7
- package/dist/board/model/index.d.ts +5 -5
- package/dist/board/model/index.js +4 -4
- package/dist/board/model/items/create.js +1 -1
- package/dist/board/model/items/definition.d.ts +9 -1
- package/dist/board/model/items/index.d.ts +2 -2
- package/dist/board/model/items/registry.d.ts +2 -1
- package/dist/board/model/items/registry.js +1 -0
- package/dist/board/model/text-metrics.d.ts +21 -1
- package/dist/board/model/text-metrics.js +24 -1
- package/dist/board/model/themes.d.ts +49 -42
- package/dist/board/model/themes.js +49 -44
- package/dist/board/render/index.d.ts +8 -6
- package/dist/board/render/index.js +7 -5
- package/dist/board/render/labels.d.ts +15 -0
- package/dist/board/render/labels.js +14 -0
- package/dist/board/render/lod.d.ts +9 -1
- package/dist/board/render/lod.js +7 -1
- package/dist/board/render/media-interaction.d.ts +5 -2
- package/dist/board/render/media-interaction.js +14 -2
- package/dist/board/render/renderers/arrow-card-renderer.js +7 -5
- package/dist/board/render/renderers/board-renderer-registry.d.ts +12 -1
- package/dist/board/render/renderers/board-renderer-registry.js +9 -4
- package/dist/board/render/renderers/far-plate.d.ts +4 -1
- package/dist/board/render/renderers/far-plate.js +5 -1
- package/dist/board/render/renderers/file-card-renderer.js +44 -24
- package/dist/board/render/renderers/frame-card-renderer.js +9 -3
- package/dist/board/render/renderers/image-card-renderer.js +2 -2
- package/dist/board/render/renderers/model-card-renderer.js +2 -2
- package/dist/board/render/renderers/task-card-renderer.js +2 -2
- package/dist/board/render/renderers/video-card-renderer.js +2 -2
- package/dist/board/render/style.d.ts +1 -0
- package/dist/board/render/style.js +3 -1
- package/dist/board/render/text-edit.d.ts +32 -0
- package/dist/board/render/text-edit.js +134 -0
- package/dist/board/render/text-fit.d.ts +1 -3
- package/dist/board/render/text-fit.js +4 -43
- package/dist/board/render/text-measurement.d.ts +12 -2
- package/dist/board/render/text-measurement.js +55 -10
- package/dist/board/replica/index.d.ts +4 -2
- package/dist/board/replica/index.js +34 -21
- package/dist/board/replica/indexeddb.d.ts +16 -0
- package/dist/board/replica/indexeddb.js +155 -0
- package/dist/board/stage/assets.d.ts +16 -4
- package/dist/board/stage/assets.js +155 -30
- package/dist/board/stage/bench.d.ts +35 -0
- package/dist/board/stage/bench.js +153 -0
- package/dist/board/stage/device.d.ts +41 -0
- package/dist/board/stage/device.js +62 -0
- package/dist/board/stage/frame-budget.d.ts +19 -0
- package/dist/board/stage/frame-budget.js +69 -0
- package/dist/board/stage/idle.d.ts +4 -0
- package/dist/board/stage/idle.js +26 -0
- package/dist/board/stage/index.d.ts +9 -5
- package/dist/board/stage/index.js +5 -2
- package/dist/board/stage/presses.d.ts +21 -0
- package/dist/board/stage/presses.js +66 -0
- package/dist/board/stage/scene.d.ts +4 -0
- package/dist/board/stage/scene.js +45 -13
- package/dist/board/stage/stage.d.ts +21 -2
- package/dist/board/stage/stage.js +309 -89
- package/dist/board/stage/texture-pool.d.ts +6 -4
- package/dist/board/stage/texture-pool.js +42 -8
- package/dist/board/stage/views.d.ts +1 -1
- package/dist/board/stage/views.js +2 -2
- package/dist/chunks/environment.d.ts +6 -0
- package/dist/chunks/http.d.ts +9 -1
- package/dist/chunks/http.js +791 -4
- package/dist/index.d.ts +52 -1
- package/dist/index.js +65 -733
- package/dist/protocol/dist/board-model.d.ts +15 -1
- package/dist/protocol/dist/board-model.js +12 -2
- package/dist/protocol/dist/board-schema.d.ts +2 -1
- package/dist/protocol/dist/board-schema.js +4 -1
- package/dist/protocol/dist/core/media.js +4 -0
- package/dist/protocol/dist/index.d.ts +2 -2
- package/package.json +2 -1
|
@@ -1,43 +1,53 @@
|
|
|
1
1
|
import { probeMediaInfo } from "../../protocol/dist/core/media.js";
|
|
2
2
|
import { MEDIA_VARIANT_SIZES, imageVariantUrl, variantSource } from "../../media.js";
|
|
3
3
|
import { boardModelFormat } from "../model/media.js";
|
|
4
|
-
import { boardAssetKey, boardAssetKeySource } from "../model/asset-key.js";
|
|
4
|
+
import { boardAssetKey, boardAssetKeySource, boardAssetRefKey } from "../model/asset-key.js";
|
|
5
5
|
import { loadVideoThumbnailTexture, videoTextureNaturalSize } from "../render/video-thumbnail.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { planLruTrim } from "./lru.js";
|
|
7
|
+
import { defaultBoardDeviceBudget } from "./device.js";
|
|
8
|
+
import { scheduleBoardIdle } from "./idle.js";
|
|
9
|
+
import { boardImageTexturePool, loadBoardImageTexture, loadFittedImageTexture, readTextureTone, shrinkBoardTexture, unloadBoardImageTexture } from "./texture-pool.js";
|
|
8
10
|
import { Texture } from "pixi.js";
|
|
9
11
|
//#region src/board/stage/assets.ts
|
|
10
|
-
function variantUrl(source, edge) {
|
|
12
|
+
function variantUrl(source, edge, cover) {
|
|
11
13
|
if (!(edge <= 1536)) return null;
|
|
12
|
-
const url = imageVariantUrl(source, edge);
|
|
14
|
+
const url = imageVariantUrl(source, edge, cover ? "cover" : "contain");
|
|
13
15
|
return url && url !== source ? url : null;
|
|
14
16
|
}
|
|
15
17
|
const SMALLEST_VARIANT = MEDIA_VARIANT_SIZES[0];
|
|
16
|
-
function
|
|
17
|
-
return
|
|
18
|
+
function boundEdge({ width, height }, cover) {
|
|
19
|
+
return cover ? Math.min(width, height) : Math.max(width, height);
|
|
20
|
+
}
|
|
21
|
+
function sharpEdge(texture, edge, resized, cover) {
|
|
22
|
+
return resized && boundEdge(texture, cover) >= edge ? edge : Number.POSITIVE_INFINITY;
|
|
18
23
|
}
|
|
19
24
|
const MAX_RETRY_DELAY = 3e4;
|
|
25
|
+
const PREFETCH_CONCURRENCY = 2;
|
|
26
|
+
const PREFETCH_POOL_SHARE = .5;
|
|
20
27
|
const SHRINK_EDGE = 256;
|
|
21
28
|
const SHRINK_CONCURRENCY = 4;
|
|
22
29
|
const TRIM_DELAY = 400;
|
|
23
30
|
const TRIM_MAX_DELAY = 2e3;
|
|
31
|
+
const TONE_LIMIT = 8192;
|
|
24
32
|
function footprintOf(texture) {
|
|
25
33
|
if (!texture) return 0;
|
|
26
34
|
return (texture.width || 0) * (texture.height || 0) * 4;
|
|
27
35
|
}
|
|
28
36
|
function createBoardAssetManager(options) {
|
|
29
|
-
const concurrency = Math.max(1, options.concurrency ??
|
|
37
|
+
const concurrency = Math.max(1, options.concurrency ?? defaultBoardDeviceBudget().concurrency);
|
|
30
38
|
const limits = {
|
|
31
39
|
image: concurrency,
|
|
32
40
|
video: Math.max(1, Math.min(options.videoConcurrency ?? 2, concurrency)),
|
|
33
41
|
model: 1
|
|
34
42
|
};
|
|
35
|
-
const budget = options.lruBudget ??
|
|
43
|
+
const budget = options.lruBudget ?? defaultBoardDeviceBudget().lru;
|
|
44
|
+
const prefetchLimit = Math.min(PREFETCH_CONCURRENCY, concurrency - 1);
|
|
36
45
|
const now = options.now ?? (() => Date.now());
|
|
37
46
|
const imagePool = options.imageTexturePool ?? boardImageTexturePool;
|
|
38
47
|
const useSharedImagePool = !options.loadTexture && !options.unloadTexture;
|
|
39
48
|
const loadTexture = options.loadTexture ?? ((url, media) => media === "video" ? loadVideoThumbnailTexture(url) : useSharedImagePool ? imagePool.acquire(url) : loadBoardImageTexture(url));
|
|
40
49
|
const shrinkTexture = options.shrinkTexture ?? (useSharedImagePool ? shrinkBoardTexture : null);
|
|
50
|
+
const readTone = options.readTone === void 0 ? typeof createImageBitmap === "function" ? readTextureTone : null : options.readTone;
|
|
41
51
|
const pendingUnloads = /* @__PURE__ */ new Map();
|
|
42
52
|
const unloadTexture = options.unloadTexture ?? (async (url, texture, media) => {
|
|
43
53
|
if (media !== "image" || !url) {
|
|
@@ -86,8 +96,39 @@ function createBoardAssetManager(options) {
|
|
|
86
96
|
let trimTimer = null;
|
|
87
97
|
let trimSince = 0;
|
|
88
98
|
let shrinking = 0;
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
let warm = /* @__PURE__ */ new Set();
|
|
100
|
+
let warming = 0;
|
|
101
|
+
const unwarmable = /* @__PURE__ */ new Set();
|
|
102
|
+
function isWarm(key) {
|
|
103
|
+
return warm.has(key) && (entries.get(key)?.refs ?? 0) <= 0;
|
|
104
|
+
}
|
|
105
|
+
function notify(key, change = "texture") {
|
|
106
|
+
for (const listener of listeners) listener(key, change);
|
|
107
|
+
}
|
|
108
|
+
const tones = /* @__PURE__ */ new Map();
|
|
109
|
+
const toneQueue = /* @__PURE__ */ new Set();
|
|
110
|
+
let cancelTone = null;
|
|
111
|
+
function queueTone(key) {
|
|
112
|
+
if (!readTone || tones.has(key)) return;
|
|
113
|
+
toneQueue.add(key);
|
|
114
|
+
cancelTone ??= scheduleBoardIdle(readNextTone, 0);
|
|
115
|
+
}
|
|
116
|
+
async function readNextTone() {
|
|
117
|
+
const [key] = toneQueue;
|
|
118
|
+
if (key === void 0) {
|
|
119
|
+
cancelTone = null;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
toneQueue.delete(key);
|
|
123
|
+
const texture = entries.get(key)?.texture;
|
|
124
|
+
const tone = texture && readTone ? await readTone(texture).catch(() => null) : null;
|
|
125
|
+
if (disposed) return;
|
|
126
|
+
if (tone !== null) {
|
|
127
|
+
if (tones.size >= TONE_LIMIT) tones.delete(tones.keys().next().value);
|
|
128
|
+
tones.set(key, tone);
|
|
129
|
+
notify(key, "tone");
|
|
130
|
+
}
|
|
131
|
+
cancelTone = toneQueue.size > 0 ? scheduleBoardIdle(readNextTone, 0) : null;
|
|
91
132
|
}
|
|
92
133
|
const keys = /* @__PURE__ */ new WeakMap();
|
|
93
134
|
let keyEpoch = 0;
|
|
@@ -102,7 +143,8 @@ function createBoardAssetManager(options) {
|
|
|
102
143
|
return key;
|
|
103
144
|
}
|
|
104
145
|
function resolveAssetKey(item) {
|
|
105
|
-
const
|
|
146
|
+
const asset = options.itemAsset?.(item);
|
|
147
|
+
const key = asset === void 0 ? boardAssetKey(item) : asset && boardAssetRefKey(asset);
|
|
106
148
|
if (!key) return null;
|
|
107
149
|
const source = boardAssetKeySource(key);
|
|
108
150
|
if (source?.media === "video" && options.loadVideoPreviews === false) return null;
|
|
@@ -120,8 +162,10 @@ function createBoardAssetManager(options) {
|
|
|
120
162
|
function ensureEntry(key) {
|
|
121
163
|
let entry = entries.get(key);
|
|
122
164
|
if (!entry) {
|
|
165
|
+
const source = boardAssetKeySource(key);
|
|
123
166
|
entry = {
|
|
124
|
-
media:
|
|
167
|
+
media: source?.media ?? "image",
|
|
168
|
+
cover: source?.fit === "cover",
|
|
125
169
|
source: null,
|
|
126
170
|
url: null,
|
|
127
171
|
texture: null,
|
|
@@ -221,6 +265,7 @@ function createBoardAssetManager(options) {
|
|
|
221
265
|
if (loaded) releaseTexture(entry.media, loaded.url, loaded.texture);
|
|
222
266
|
return;
|
|
223
267
|
}
|
|
268
|
+
if (warm.delete(key) && !loaded && !entry.texture && entry.refs <= 0) unwarmable.add(key);
|
|
224
269
|
if (loaded) {
|
|
225
270
|
const replaced = entry.texture;
|
|
226
271
|
const replacedUrl = entry.url ?? "";
|
|
@@ -238,6 +283,7 @@ function createBoardAssetManager(options) {
|
|
|
238
283
|
if (entry.refs <= 0) scheduleTrim();
|
|
239
284
|
else notify(key);
|
|
240
285
|
if (replaced) retire(entry.media, replacedUrl, replaced);
|
|
286
|
+
queueTone(key);
|
|
241
287
|
refine(key, entry);
|
|
242
288
|
return;
|
|
243
289
|
}
|
|
@@ -258,11 +304,11 @@ function createBoardAssetManager(options) {
|
|
|
258
304
|
notify(key);
|
|
259
305
|
}
|
|
260
306
|
}
|
|
261
|
-
function refinable(entry) {
|
|
262
|
-
return entry.media === "image" && entry.refs > 0 && entry.texture !== null && entry.source !== null && entry.want > entry.edge;
|
|
307
|
+
function refinable(key, entry) {
|
|
308
|
+
return entry.media === "image" && (entry.refs > 0 || warm.has(key)) && entry.texture !== null && entry.source !== null && entry.want > entry.edge;
|
|
263
309
|
}
|
|
264
310
|
function refine(key, entry) {
|
|
265
|
-
if (refinable(entry) && !entry.loading && entry.source) enqueue(key, "image", textureLoad("image", entry.source));
|
|
311
|
+
if (refinable(key, entry) && !entry.loading && entry.source) enqueue(key, "image", textureLoad("image", entry.source));
|
|
266
312
|
}
|
|
267
313
|
function enqueue(key, media, load) {
|
|
268
314
|
if (queue.some((task) => task.key === key)) return;
|
|
@@ -275,16 +321,20 @@ function createBoardAssetManager(options) {
|
|
|
275
321
|
}
|
|
276
322
|
function nextTask() {
|
|
277
323
|
let best = -1;
|
|
324
|
+
let bestWarm = true;
|
|
278
325
|
let nearest = Number.POSITIVE_INFINITY;
|
|
279
326
|
for (let index = 0; index < queue.length; index += 1) {
|
|
280
327
|
const task = queue[index];
|
|
281
328
|
if (activeByMedia[task.media] >= limits[task.media]) continue;
|
|
329
|
+
const prefetching = isWarm(task.key);
|
|
330
|
+
if (prefetching && (!bestWarm || warming >= prefetchLimit)) continue;
|
|
282
331
|
const center = entries.get(task.key)?.center;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (distance < nearest) {
|
|
332
|
+
const distance = focus && center ? (center.x - focus.x) ** 2 + (center.y - focus.y) ** 2 : 0;
|
|
333
|
+
if (best < 0 || bestWarm && !prefetching || distance < nearest) {
|
|
286
334
|
nearest = distance;
|
|
287
335
|
best = index;
|
|
336
|
+
bestWarm = prefetching;
|
|
337
|
+
if (!focus && !prefetching) return index;
|
|
288
338
|
}
|
|
289
339
|
}
|
|
290
340
|
return best;
|
|
@@ -296,13 +346,16 @@ function createBoardAssetManager(options) {
|
|
|
296
346
|
const [task] = queue.splice(taskIndex, 1);
|
|
297
347
|
if (!task) continue;
|
|
298
348
|
const entry = entries.get(task.key);
|
|
299
|
-
if (!entry || entry.loading || entry.texture && !refinable(entry)) continue;
|
|
349
|
+
if (!entry || entry.loading || entry.texture && !refinable(task.key, entry)) continue;
|
|
350
|
+
const prefetching = isWarm(task.key);
|
|
300
351
|
entry.loading = true;
|
|
301
352
|
active += 1;
|
|
302
353
|
activeByMedia[task.media] += 1;
|
|
354
|
+
if (prefetching) warming += 1;
|
|
303
355
|
const done = (loaded, error) => {
|
|
304
356
|
active -= 1;
|
|
305
357
|
activeByMedia[task.media] -= 1;
|
|
358
|
+
if (prefetching) warming -= 1;
|
|
306
359
|
adopt(task.key, entry, loaded, error);
|
|
307
360
|
if (!disposed) pump();
|
|
308
361
|
};
|
|
@@ -312,24 +365,26 @@ function createBoardAssetManager(options) {
|
|
|
312
365
|
function textureLoad(media, source) {
|
|
313
366
|
return async (entry) => {
|
|
314
367
|
entry.source = source;
|
|
315
|
-
const edge = entry
|
|
316
|
-
const variant = media === "image" ? variantUrl(source, Math.max(edge, SMALLEST_VARIANT)) : null;
|
|
368
|
+
const { cover, want: edge } = entry;
|
|
369
|
+
const variant = media === "image" ? variantUrl(source, Math.max(edge, SMALLEST_VARIANT), cover) : null;
|
|
317
370
|
const url = variant ?? source;
|
|
318
371
|
const pending = pendingUnloads.get(`${media}:${url}`);
|
|
319
372
|
if (pending) await pending;
|
|
320
373
|
if (disposed) return null;
|
|
321
374
|
if (media === "image" && !variant && useSharedImagePool && Number.isFinite(edge)) {
|
|
322
|
-
const fitted = await loadFittedImageTexture(url, edge).catch(() => null);
|
|
375
|
+
const fitted = await loadFittedImageTexture(url, edge, cover).catch(() => null);
|
|
323
376
|
if (fitted) return {
|
|
324
377
|
texture: fitted,
|
|
325
378
|
url: "",
|
|
326
|
-
edge: sharpEdge(fitted, edge, true)
|
|
379
|
+
edge: sharpEdge(fitted, edge, true, cover)
|
|
327
380
|
};
|
|
328
381
|
}
|
|
329
382
|
const texture = await loadTexture(url, media);
|
|
330
383
|
if (!texture) return null;
|
|
331
|
-
|
|
332
|
-
|
|
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);
|
|
333
388
|
if (small) {
|
|
334
389
|
releaseTexture(media, url, texture);
|
|
335
390
|
return {
|
|
@@ -342,7 +397,7 @@ function createBoardAssetManager(options) {
|
|
|
342
397
|
return {
|
|
343
398
|
texture,
|
|
344
399
|
url,
|
|
345
|
-
edge: sharpEdge(texture, edge, Boolean(variant))
|
|
400
|
+
edge: sharpEdge(texture, edge, Boolean(variant), cover)
|
|
346
401
|
};
|
|
347
402
|
};
|
|
348
403
|
}
|
|
@@ -352,7 +407,7 @@ function createBoardAssetManager(options) {
|
|
|
352
407
|
entry.loading = true;
|
|
353
408
|
resolveFileUrl(path, fresh ? { fresh } : void 0).then((url) => {
|
|
354
409
|
if (disposed || entries.get(key) !== entry) return;
|
|
355
|
-
if (entry.refs <= 0) return evict(key, entry);
|
|
410
|
+
if (entry.refs <= 0 && !warm.has(key)) return evict(key, entry);
|
|
356
411
|
if (!url) return adopt(key, entry, null, /* @__PURE__ */ new Error(`Unavailable file: ${path}`));
|
|
357
412
|
entry.loading = false;
|
|
358
413
|
enqueue(key, media, textureLoad(media, url));
|
|
@@ -419,6 +474,9 @@ function createBoardAssetManager(options) {
|
|
|
419
474
|
keyEpoch += 1;
|
|
420
475
|
for (const key of stale) notify(key);
|
|
421
476
|
},
|
|
477
|
+
invalidateItemAssets() {
|
|
478
|
+
if (options.itemAsset) keyEpoch += 1;
|
|
479
|
+
},
|
|
422
480
|
requestItem(item, edge = Number.POSITIVE_INFINITY) {
|
|
423
481
|
if (disposed) return;
|
|
424
482
|
const key = managedAssetKey(item);
|
|
@@ -457,10 +515,14 @@ function createBoardAssetManager(options) {
|
|
|
457
515
|
hasError(key) {
|
|
458
516
|
return entries.get(key)?.error ?? false;
|
|
459
517
|
},
|
|
518
|
+
getTone(key) {
|
|
519
|
+
return tones.get(key) ?? null;
|
|
520
|
+
},
|
|
460
521
|
focus(point) {
|
|
461
522
|
focus = point;
|
|
462
523
|
},
|
|
463
524
|
acquire(key) {
|
|
525
|
+
unwarmable.delete(key);
|
|
464
526
|
const entry = ensureEntry(key);
|
|
465
527
|
if (entry.refs <= 0) entry.want = 0;
|
|
466
528
|
entry.refs += 1;
|
|
@@ -477,6 +539,55 @@ function createBoardAssetManager(options) {
|
|
|
477
539
|
if (!entry.texture) evict(key, entry);
|
|
478
540
|
else scheduleTrim();
|
|
479
541
|
},
|
|
542
|
+
prefetch(requests) {
|
|
543
|
+
if (disposed) return;
|
|
544
|
+
const next = /* @__PURE__ */ new Set();
|
|
545
|
+
let room = budget.maxBytes * PREFETCH_POOL_SHARE;
|
|
546
|
+
for (const entry of entries.values()) if (entry.refs <= 0) room -= footprintOf(entry.texture);
|
|
547
|
+
for (const { item, edge } of requests) {
|
|
548
|
+
const key = managedAssetKey(item);
|
|
549
|
+
if (!key || !Number.isFinite(edge) || unwarmable.has(key) || boardAssetKeySource(key)?.media !== "image") continue;
|
|
550
|
+
const entry = entries.get(key);
|
|
551
|
+
const sharp = entry?.texture && entry.edge >= edge;
|
|
552
|
+
if (entry && (entry.refs > 0 || sharp || entry.error || entry.loading && !warm.has(key))) continue;
|
|
553
|
+
room -= edge * edge * 4;
|
|
554
|
+
if (room < 0) break;
|
|
555
|
+
next.add(key);
|
|
556
|
+
}
|
|
557
|
+
for (const key of warm) {
|
|
558
|
+
if (next.has(key)) continue;
|
|
559
|
+
const entry = entries.get(key);
|
|
560
|
+
if (!entry || entry.refs > 0 || entry.loading) continue;
|
|
561
|
+
const index = queue.findIndex((task) => task.key === key);
|
|
562
|
+
if (index >= 0) queue.splice(index, 1);
|
|
563
|
+
if (!entry.texture) evict(key, entry);
|
|
564
|
+
}
|
|
565
|
+
const fresh = new Set([...next].filter((key) => !warm.has(key)));
|
|
566
|
+
warm = next;
|
|
567
|
+
for (const { item, edge } of requests) {
|
|
568
|
+
const key = managedAssetKey(item);
|
|
569
|
+
if (!key || !next.has(key)) continue;
|
|
570
|
+
const entry = ensureEntry(key);
|
|
571
|
+
entry.want = edge;
|
|
572
|
+
entry.center = {
|
|
573
|
+
x: item.frame.x + item.frame.width / 2,
|
|
574
|
+
y: item.frame.y + item.frame.height / 2
|
|
575
|
+
};
|
|
576
|
+
if (!fresh.delete(key)) continue;
|
|
577
|
+
if (entry.texture) {
|
|
578
|
+
refine(key, entry);
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const start = starter(item, key, boardAssetKeySource(key));
|
|
582
|
+
if (!start) {
|
|
583
|
+
warm.delete(key);
|
|
584
|
+
evict(key, entry);
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
starters.set(key, start);
|
|
588
|
+
start(entry);
|
|
589
|
+
}
|
|
590
|
+
},
|
|
480
591
|
async withTextures(items, use, loadOptions) {
|
|
481
592
|
const keys = /* @__PURE__ */ new Set();
|
|
482
593
|
if (!disposed) for (const item of items) {
|
|
@@ -490,7 +601,7 @@ function createBoardAssetManager(options) {
|
|
|
490
601
|
for (const item of items) manager.requestItem(item, loadOptions?.edge?.(item));
|
|
491
602
|
const settled = () => [...keys].every((key) => {
|
|
492
603
|
const entry = entries.get(key);
|
|
493
|
-
return Boolean(entry?.error) || Boolean(entry?.texture && !refinable(entry));
|
|
604
|
+
return Boolean(entry?.error) || Boolean(entry?.texture && !refinable(key, entry));
|
|
494
605
|
});
|
|
495
606
|
if (!settled()) {
|
|
496
607
|
const timeoutMs = loadOptions?.timeoutMs ?? 15e3;
|
|
@@ -525,10 +636,19 @@ function createBoardAssetManager(options) {
|
|
|
525
636
|
if (entry.refs <= 0) cooling += 1;
|
|
526
637
|
bytes += footprintOf(entry.texture);
|
|
527
638
|
}
|
|
639
|
+
let loading = active - warming;
|
|
640
|
+
let prefetching = warming;
|
|
641
|
+
for (const task of queue) {
|
|
642
|
+
if (!entries.has(task.key)) continue;
|
|
643
|
+
if (isWarm(task.key)) prefetching += 1;
|
|
644
|
+
else loading += 1;
|
|
645
|
+
}
|
|
528
646
|
return {
|
|
529
647
|
textures,
|
|
530
648
|
cooling,
|
|
531
|
-
bytes
|
|
649
|
+
bytes,
|
|
650
|
+
loading,
|
|
651
|
+
prefetching
|
|
532
652
|
};
|
|
533
653
|
},
|
|
534
654
|
subscribe(listener) {
|
|
@@ -541,7 +661,12 @@ function createBoardAssetManager(options) {
|
|
|
541
661
|
disposed = true;
|
|
542
662
|
if (trimTimer !== null) clearTimeout(trimTimer);
|
|
543
663
|
trimTimer = null;
|
|
664
|
+
cancelTone?.();
|
|
665
|
+
toneQueue.clear();
|
|
666
|
+
tones.clear();
|
|
544
667
|
queue.length = 0;
|
|
668
|
+
warm.clear();
|
|
669
|
+
unwarmable.clear();
|
|
545
670
|
listeners.clear();
|
|
546
671
|
pathVersions.clear();
|
|
547
672
|
starters.clear();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { BoardStage } from "./stage.js";
|
|
2
|
+
//#region src/board/stage/bench.d.ts
|
|
3
|
+
type BoardBenchPhase = {
|
|
4
|
+
name: string;
|
|
5
|
+
frames: number;
|
|
6
|
+
wheel: Pick<WheelEventInit, "deltaX" | "deltaY" | "ctrlKey">;
|
|
7
|
+
rest?: number;
|
|
8
|
+
};
|
|
9
|
+
type BoardBenchPercentiles = {
|
|
10
|
+
p50: number;
|
|
11
|
+
p95: number;
|
|
12
|
+
max: number;
|
|
13
|
+
};
|
|
14
|
+
type BoardBenchSettle = {
|
|
15
|
+
shown: number;
|
|
16
|
+
ready: number;
|
|
17
|
+
loaded: number;
|
|
18
|
+
};
|
|
19
|
+
type BoardBenchPhaseReport = BoardBenchSettle & {
|
|
20
|
+
name: string;
|
|
21
|
+
frame: BoardBenchPercentiles;
|
|
22
|
+
work: BoardBenchPercentiles;
|
|
23
|
+
long: number;
|
|
24
|
+
};
|
|
25
|
+
type BoardBenchReport = {
|
|
26
|
+
open: BoardBenchSettle;
|
|
27
|
+
phases: BoardBenchPhaseReport[];
|
|
28
|
+
};
|
|
29
|
+
declare const BOARD_BENCH_PHASES: readonly BoardBenchPhase[];
|
|
30
|
+
declare function runBoardStageBench(stage: BoardStage, { since, phases }?: {
|
|
31
|
+
since?: number;
|
|
32
|
+
phases?: readonly BoardBenchPhase[];
|
|
33
|
+
}): Promise<BoardBenchReport>;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { BOARD_BENCH_PHASES, BoardBenchPercentiles, BoardBenchPhase, BoardBenchPhaseReport, BoardBenchReport, BoardBenchSettle, runBoardStageBench };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
//#region src/board/stage/bench.ts
|
|
2
|
+
const STEP = {
|
|
3
|
+
name: "step",
|
|
4
|
+
frames: 1,
|
|
5
|
+
rest: 3e3,
|
|
6
|
+
wheel: { deltaY: 960 }
|
|
7
|
+
};
|
|
8
|
+
const BOARD_BENCH_PHASES = [
|
|
9
|
+
{
|
|
10
|
+
name: "zoom in",
|
|
11
|
+
frames: 15,
|
|
12
|
+
wheel: {
|
|
13
|
+
deltaY: -10,
|
|
14
|
+
ctrlKey: true
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "close",
|
|
19
|
+
frames: 12,
|
|
20
|
+
wheel: {
|
|
21
|
+
deltaY: -10,
|
|
22
|
+
ctrlKey: true
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
STEP,
|
|
26
|
+
STEP,
|
|
27
|
+
STEP,
|
|
28
|
+
{
|
|
29
|
+
name: "pan close",
|
|
30
|
+
frames: 60,
|
|
31
|
+
wheel: {
|
|
32
|
+
deltaX: -24,
|
|
33
|
+
deltaY: 16
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "zoom out",
|
|
38
|
+
frames: 12,
|
|
39
|
+
wheel: {
|
|
40
|
+
deltaY: 10,
|
|
41
|
+
ctrlKey: true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "pan",
|
|
46
|
+
frames: 120,
|
|
47
|
+
wheel: { deltaX: 24 }
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "overview",
|
|
51
|
+
frames: 16,
|
|
52
|
+
wheel: {
|
|
53
|
+
deltaY: 10,
|
|
54
|
+
ctrlKey: true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
];
|
|
58
|
+
const SETTLE_LIMIT_MS = 2e4;
|
|
59
|
+
const LONG_FRAME_MS = 50;
|
|
60
|
+
function percentiles(values) {
|
|
61
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
62
|
+
const at = (ratio) => sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * ratio))] ?? 0;
|
|
63
|
+
return {
|
|
64
|
+
p50: at(.5),
|
|
65
|
+
p95: at(.95),
|
|
66
|
+
max: sorted[sorted.length - 1] ?? 0
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const nextFrame = () => new Promise((resolve) => requestAnimationFrame(resolve));
|
|
70
|
+
async function settle(stage, since) {
|
|
71
|
+
const times = {};
|
|
72
|
+
for (;;) {
|
|
73
|
+
const now = await nextFrame();
|
|
74
|
+
const { waiting, settling, loading } = stage.stats();
|
|
75
|
+
const at = now - since;
|
|
76
|
+
if (times.shown === void 0 && waiting === 0) times.shown = at;
|
|
77
|
+
if (times.ready === void 0 && !settling) times.ready = at;
|
|
78
|
+
if (times.loaded === void 0 && !settling && loading === 0) times.loaded = at;
|
|
79
|
+
if (times.loaded !== void 0 || at >= SETTLE_LIMIT_MS) return {
|
|
80
|
+
shown: times.shown ?? SETTLE_LIMIT_MS,
|
|
81
|
+
ready: times.ready ?? SETTLE_LIMIT_MS,
|
|
82
|
+
loaded: times.loaded ?? SETTLE_LIMIT_MS
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function measureFrameWork() {
|
|
87
|
+
const original = globalThis.requestAnimationFrame;
|
|
88
|
+
let work = 0;
|
|
89
|
+
globalThis.requestAnimationFrame = (callback) => original((time) => {
|
|
90
|
+
const start = performance.now();
|
|
91
|
+
try {
|
|
92
|
+
callback(time);
|
|
93
|
+
} finally {
|
|
94
|
+
work += performance.now() - start;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
take: () => {
|
|
99
|
+
const taken = work;
|
|
100
|
+
work = 0;
|
|
101
|
+
return taken;
|
|
102
|
+
},
|
|
103
|
+
stop: () => {
|
|
104
|
+
globalThis.requestAnimationFrame = original;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async function runBoardStageBench(stage, { since = performance.now(), phases = BOARD_BENCH_PHASES } = {}) {
|
|
109
|
+
await stage.ready;
|
|
110
|
+
const canvas = stage.renderer?.canvas;
|
|
111
|
+
if (!canvas) throw new Error("Board stage has no canvas.");
|
|
112
|
+
const open = await settle(stage, since);
|
|
113
|
+
const rect = canvas.getBoundingClientRect();
|
|
114
|
+
const work = measureFrameWork();
|
|
115
|
+
try {
|
|
116
|
+
const reports = [];
|
|
117
|
+
for (const phase of phases) {
|
|
118
|
+
if (phase.rest) await new Promise((resolve) => setTimeout(resolve, phase.rest));
|
|
119
|
+
const intervals = [];
|
|
120
|
+
const costs = [];
|
|
121
|
+
let previous = await nextFrame();
|
|
122
|
+
for (let frame = 0; frame < phase.frames; frame += 1) {
|
|
123
|
+
canvas.dispatchEvent(new WheelEvent("wheel", {
|
|
124
|
+
bubbles: true,
|
|
125
|
+
cancelable: true,
|
|
126
|
+
clientX: rect.left + rect.width / 2,
|
|
127
|
+
clientY: rect.top + rect.height / 2,
|
|
128
|
+
...phase.wheel
|
|
129
|
+
}));
|
|
130
|
+
work.take();
|
|
131
|
+
const now = await nextFrame();
|
|
132
|
+
costs.push(work.take());
|
|
133
|
+
intervals.push(now - previous);
|
|
134
|
+
previous = now;
|
|
135
|
+
}
|
|
136
|
+
reports.push({
|
|
137
|
+
name: phase.name,
|
|
138
|
+
frame: percentiles(intervals),
|
|
139
|
+
work: percentiles(costs),
|
|
140
|
+
long: intervals.filter((interval) => interval > LONG_FRAME_MS).length,
|
|
141
|
+
...await settle(stage, previous)
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
open,
|
|
146
|
+
phases: reports
|
|
147
|
+
};
|
|
148
|
+
} finally {
|
|
149
|
+
work.stop();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
export { BOARD_BENCH_PHASES, runBoardStageBench };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { BoardLod } from "../render/lod.js";
|
|
2
|
+
import { LruBudget } from "./lru.js";
|
|
3
|
+
//#region src/board/stage/device.d.ts
|
|
4
|
+
type BoardDeviceTier = "low" | "standard" | "high";
|
|
5
|
+
type BoardDeviceSignals = {
|
|
6
|
+
phone?: boolean;
|
|
7
|
+
memory?: number;
|
|
8
|
+
cores?: number;
|
|
9
|
+
saveData?: boolean;
|
|
10
|
+
slowNetwork?: boolean;
|
|
11
|
+
};
|
|
12
|
+
type BoardDeviceBudget = {
|
|
13
|
+
tier: BoardDeviceTier;
|
|
14
|
+
lod: BoardLod;
|
|
15
|
+
lru: LruBudget;
|
|
16
|
+
concurrency: number;
|
|
17
|
+
prefetch: number;
|
|
18
|
+
};
|
|
19
|
+
declare const BOARD_DEVICE_TIERS: readonly BoardDeviceTier[];
|
|
20
|
+
type BoardDeviceEnvironment = {
|
|
21
|
+
navigator?: Navigator & {
|
|
22
|
+
deviceMemory?: number;
|
|
23
|
+
connection?: {
|
|
24
|
+
saveData?: boolean;
|
|
25
|
+
effectiveType?: string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
matchMedia?: (query: string) => {
|
|
29
|
+
matches: boolean;
|
|
30
|
+
};
|
|
31
|
+
screen?: {
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
declare function readBoardDeviceSignals(environment?: BoardDeviceEnvironment): BoardDeviceSignals;
|
|
37
|
+
declare function boardDeviceTier({ phone, memory, cores }: BoardDeviceSignals): BoardDeviceTier;
|
|
38
|
+
declare function boardDeviceBudget(signals: BoardDeviceSignals, tier?: BoardDeviceTier): BoardDeviceBudget;
|
|
39
|
+
declare function defaultBoardDeviceBudget(): BoardDeviceBudget;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { BOARD_DEVICE_TIERS, BoardDeviceBudget, BoardDeviceEnvironment, BoardDeviceSignals, BoardDeviceTier, boardDeviceBudget, boardDeviceTier, defaultBoardDeviceBudget, readBoardDeviceSignals };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { COMPACT_BOARD_LOD, DEFAULT_BOARD_LOD } from "../render/lod.js";
|
|
2
|
+
import { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET } from "./lru.js";
|
|
3
|
+
//#region src/board/stage/device.ts
|
|
4
|
+
const BUDGETS = {
|
|
5
|
+
low: {
|
|
6
|
+
lod: COMPACT_BOARD_LOD,
|
|
7
|
+
lru: COMPACT_LRU_BUDGET,
|
|
8
|
+
concurrency: 4,
|
|
9
|
+
prefetch: 0
|
|
10
|
+
},
|
|
11
|
+
standard: {
|
|
12
|
+
lod: DEFAULT_BOARD_LOD,
|
|
13
|
+
lru: DEFAULT_LRU_BUDGET,
|
|
14
|
+
concurrency: 6,
|
|
15
|
+
prefetch: .5
|
|
16
|
+
},
|
|
17
|
+
high: {
|
|
18
|
+
lod: DEFAULT_BOARD_LOD,
|
|
19
|
+
lru: {
|
|
20
|
+
maxCount: 4096,
|
|
21
|
+
maxBytes: 402653184
|
|
22
|
+
},
|
|
23
|
+
concurrency: 8,
|
|
24
|
+
prefetch: 1
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const BOARD_DEVICE_TIERS = Object.keys(BUDGETS);
|
|
28
|
+
const PHONE_SHORT_SIDE = 600;
|
|
29
|
+
function readBoardDeviceSignals(environment = globalThis) {
|
|
30
|
+
const { navigator: device, matchMedia, screen } = environment;
|
|
31
|
+
if (!device || !matchMedia || !screen) return {};
|
|
32
|
+
const connection = device.connection;
|
|
33
|
+
return {
|
|
34
|
+
phone: matchMedia("(pointer: coarse)").matches && !matchMedia("(any-pointer: fine)").matches && Math.min(screen.width, screen.height) < PHONE_SHORT_SIDE,
|
|
35
|
+
memory: device.deviceMemory,
|
|
36
|
+
cores: device.hardwareConcurrency,
|
|
37
|
+
saveData: Boolean(connection?.saveData),
|
|
38
|
+
slowNetwork: /^(slow-2g|2g|3g)$/.test(connection?.effectiveType ?? "")
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function boardDeviceTier({ phone, memory, cores }) {
|
|
42
|
+
if (memory !== void 0 && memory <= 2 || cores !== void 0 && cores <= 2) return "low";
|
|
43
|
+
if (phone) return memory !== void 0 && memory >= 8 ? "standard" : "low";
|
|
44
|
+
if (memory !== void 0 && memory <= 4 && cores !== void 0 && cores <= 4) return "low";
|
|
45
|
+
if (memory !== void 0 && memory >= 8 && cores !== void 0 && cores >= 8) return "high";
|
|
46
|
+
return "standard";
|
|
47
|
+
}
|
|
48
|
+
function boardDeviceBudget(signals, tier = boardDeviceTier(signals)) {
|
|
49
|
+
const budget = BUDGETS[tier];
|
|
50
|
+
return {
|
|
51
|
+
tier,
|
|
52
|
+
...budget,
|
|
53
|
+
prefetch: signals.saveData || signals.slowNetwork ? 0 : budget.prefetch
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
let detected;
|
|
57
|
+
function defaultBoardDeviceBudget() {
|
|
58
|
+
detected ??= boardDeviceBudget(readBoardDeviceSignals());
|
|
59
|
+
return detected;
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { BOARD_DEVICE_TIERS, boardDeviceBudget, boardDeviceTier, defaultBoardDeviceBudget, readBoardDeviceSignals };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/board/stage/frame-budget.d.ts
|
|
2
|
+
type BoardFrameWork = "materialize" | "plate" | "resolution";
|
|
3
|
+
type BoardFrameBudget = {
|
|
4
|
+
allow(kind: BoardFrameWork): number;
|
|
5
|
+
spend(kind: BoardFrameWork, units: number, ms: number): void;
|
|
6
|
+
};
|
|
7
|
+
type BoardFrameClock = BoardFrameBudget & {
|
|
8
|
+
begin(frameTime: number, options: {
|
|
9
|
+
moving: boolean;
|
|
10
|
+
chained: boolean;
|
|
11
|
+
}): void;
|
|
12
|
+
readonly interval: number;
|
|
13
|
+
};
|
|
14
|
+
declare function fixedBoardFrameBudget(): BoardFrameBudget & {
|
|
15
|
+
reset(): void;
|
|
16
|
+
};
|
|
17
|
+
declare function createBoardFrameClock(now?: () => number): BoardFrameClock;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { BoardFrameBudget, BoardFrameClock, BoardFrameWork, createBoardFrameClock, fixedBoardFrameBudget };
|