@neta-art/cohub 12.6.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 +19 -4
- 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 +3 -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 +2 -2
- package/dist/board/replica/index.js +38 -39
- package/dist/board/stage/assets.d.ts +2 -2
- package/dist/board/stage/assets.js +136 -65
- 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 +1 -1
- package/dist/board/stage/stage.d.ts +7 -2
- package/dist/board/stage/stage.js +45 -25
- package/dist/board/stage/texture-pool.d.ts +1 -1
- package/dist/board/stage/texture-pool.js +4 -5
- package/dist/chunks/environment.d.ts +26 -15
- package/dist/chunks/http.d.ts +31 -23
- package/dist/chunks/http.js +1205 -46
- package/dist/chunks/websocket.d.ts +1 -1
- package/dist/http.d.ts +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1077
- 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 +2 -0
- 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 +2 -2
- 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,10 +1,10 @@
|
|
|
1
1
|
import { BOARD_FONT_STACK, BOARD_MONO_FONT_STACK } from "../../../protocol/dist/board-constants.js";
|
|
2
2
|
import { featuredTaskArtifact } from "../../../protocol/dist/board-task.js";
|
|
3
|
-
import {
|
|
3
|
+
import { drawAudioWaveform } from "../audio-waveform.js";
|
|
4
4
|
import { fitTextToLines, fitTextToWidth } from "../text-fit.js";
|
|
5
5
|
import { syncTextResolution } from "../text-resolution.js";
|
|
6
6
|
import { createCardTag, formatBoardDuration, syncCardTag, zoomCardTag } from "../card-tag.js";
|
|
7
|
-
import {
|
|
7
|
+
import { boardCardDetailed } from "../lod.js";
|
|
8
8
|
import { cardElevation, containRect, drawCardSurface, traceCardOutline } from "../surface.js";
|
|
9
9
|
import { positionShell } from "./base-card-renderer.js";
|
|
10
10
|
import { drawFarPlate, farPictureTone } from "./far-plate.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { estimateTextWidth, layoutBoardText } from "../../../protocol/dist/board-layout.js";
|
|
2
|
-
import { itemAlpha, itemColor } from "../palette.js";
|
|
3
2
|
import { boardTextStyle, canvasFont, cssBaselineShift, layoutTextLines } from "../text-measurement.js";
|
|
4
|
-
import { boardTextSize } from "../lod.js";
|
|
5
3
|
import { syncTextResolution } from "../text-resolution.js";
|
|
4
|
+
import { itemAlpha, itemColor } from "../palette.js";
|
|
5
|
+
import { boardTextSize } from "../lod.js";
|
|
6
6
|
import { positionShell } from "./base-card-renderer.js";
|
|
7
7
|
import { drawFarBars } from "./far-plate.js";
|
|
8
8
|
import { Container, Graphics, Text } from "pixi.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getMediaResourceTitle } from "../../model/media.js";
|
|
2
|
-
import { boardCardDetailed } from "../lod.js";
|
|
3
2
|
import { createCardTag, formatBoardDuration, syncCardTag, zoomCardTag } from "../card-tag.js";
|
|
3
|
+
import { boardCardDetailed } from "../lod.js";
|
|
4
4
|
import { cardElevation, containRect, drawCardSurface, fillCardPicture } from "../surface.js";
|
|
5
5
|
import { positionShell } from "./base-card-renderer.js";
|
|
6
6
|
import { drawFarPlate, farPictureTone } from "./far-plate.js";
|
|
@@ -2,12 +2,14 @@ import { BoardShapeColors } from "../model/palette.js";
|
|
|
2
2
|
import { BoardThemeId } from "../model/themes.js";
|
|
3
3
|
import { BoardRenderStyle } from "./style.js";
|
|
4
4
|
import { BoardRenderPalette } from "./renderers/board-renderer-registry.js";
|
|
5
|
+
import { BoardThemeBackdrop } from "./backdrop.js";
|
|
5
6
|
//#region src/board/render/theme.d.ts
|
|
6
7
|
type BoardRenderTheme = {
|
|
7
8
|
colorScheme: "dark" | "light";
|
|
8
9
|
palette: BoardRenderPalette;
|
|
9
10
|
colors: BoardShapeColors;
|
|
10
11
|
style: BoardRenderStyle;
|
|
12
|
+
backdrop?: BoardThemeBackdrop;
|
|
11
13
|
};
|
|
12
14
|
/** A built-in Cohub theme as the app draws it; spread it into export or sampler options. */
|
|
13
15
|
declare function boardRenderTheme(id: BoardThemeId): BoardRenderTheme;
|
|
@@ -3,12 +3,14 @@ import { resolveBoardRenderStyle } from "./style.js";
|
|
|
3
3
|
//#region src/board/render/theme.ts
|
|
4
4
|
/** A built-in Cohub theme as the app draws it; spread it into export or sampler options. */
|
|
5
5
|
function boardRenderTheme(id) {
|
|
6
|
-
const
|
|
6
|
+
const data = BOARD_THEME_DATA[id];
|
|
7
|
+
const { colorScheme, palette, colors, style } = data;
|
|
7
8
|
return {
|
|
8
9
|
colorScheme,
|
|
9
10
|
palette: { ...palette },
|
|
10
11
|
colors: Object.fromEntries(Object.entries(colors).map(([key, value]) => [key, { ...value }])),
|
|
11
|
-
style: resolveBoardRenderStyle(colorScheme, style)
|
|
12
|
+
style: resolveBoardRenderStyle(colorScheme, style),
|
|
13
|
+
..."backdrop" in data ? { backdrop: structuredClone(data.backdrop) } : {}
|
|
12
14
|
};
|
|
13
15
|
}
|
|
14
16
|
//#endregion
|
|
@@ -14,8 +14,8 @@ type BoardChangedPayload = {
|
|
|
14
14
|
after?: BoardDelta;
|
|
15
15
|
};
|
|
16
16
|
type BoardRemote = {
|
|
17
|
-
/** `only
|
|
18
|
-
get(read?: Pick<BoardReadInput, "
|
|
17
|
+
/** `since` asks only for the changes after that version; remotes may return the full Board. */
|
|
18
|
+
get(read?: Pick<BoardReadInput, "since">): Promise<BoardReadResult>;
|
|
19
19
|
apply(patch: BoardPatch, options: {
|
|
20
20
|
mutationId: string;
|
|
21
21
|
clientId?: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { applyBoardDelta, applyBoardPatchToDocument, parseBoardDocument } from "../../protocol/dist/board-patch.js";
|
|
2
2
|
import { createBoardEntityId } from "../model/items/id.js";
|
|
3
|
-
import {
|
|
3
|
+
import { readBoardDocument } from "../model/read.js";
|
|
4
4
|
import { clearBoardReplicaStorage, createIndexedDbBoardReplicaStorage } from "./indexeddb.js";
|
|
5
5
|
//#region src/board/replica/index.ts
|
|
6
6
|
function memoryStorage() {
|
|
@@ -15,6 +15,7 @@ function memoryStorage() {
|
|
|
15
15
|
const RETRY_BASE_MS = 1e3;
|
|
16
16
|
const RETRY_MAX_MS = 3e4;
|
|
17
17
|
const PERSIST_INTERVAL_MS = 500;
|
|
18
|
+
const CACHE_WAIT_MS = 250;
|
|
18
19
|
function isRejection(error) {
|
|
19
20
|
const status = error.status;
|
|
20
21
|
return typeof status === "number" && status >= 400 && status < 500 && ![
|
|
@@ -34,6 +35,7 @@ function createBoardReplica(options) {
|
|
|
34
35
|
let unsubscribeRemote = null;
|
|
35
36
|
let server = null;
|
|
36
37
|
let version = 0;
|
|
38
|
+
let sent = [];
|
|
37
39
|
let title = null;
|
|
38
40
|
let playback = null;
|
|
39
41
|
let playbackRevision = 0;
|
|
@@ -57,7 +59,7 @@ function createBoardReplica(options) {
|
|
|
57
59
|
return remote.subscribe({
|
|
58
60
|
changed: (event) => receive(event.payload),
|
|
59
61
|
playback: (event) => receivePlayback(event.payload.playback),
|
|
60
|
-
resync: () => void
|
|
62
|
+
resync: () => void retry()
|
|
61
63
|
});
|
|
62
64
|
} catch {
|
|
63
65
|
return null;
|
|
@@ -69,7 +71,7 @@ function createBoardReplica(options) {
|
|
|
69
71
|
return;
|
|
70
72
|
}
|
|
71
73
|
let next = server;
|
|
72
|
-
for (const entry of pending) {
|
|
74
|
+
for (const entry of [...sent, ...pending]) {
|
|
73
75
|
const result = applyBoardPatchToDocument(next, entry.patch);
|
|
74
76
|
if (result.ok) next = result.document;
|
|
75
77
|
}
|
|
@@ -95,6 +97,7 @@ function createBoardReplica(options) {
|
|
|
95
97
|
function adoptServer(next, nextVersion, persist) {
|
|
96
98
|
server = next;
|
|
97
99
|
version = nextVersion;
|
|
100
|
+
sent = sent.filter((entry) => entry.version > nextVersion);
|
|
98
101
|
recompute();
|
|
99
102
|
if (persist) schedulePersist(nextVersion, next);
|
|
100
103
|
}
|
|
@@ -112,21 +115,16 @@ function createBoardReplica(options) {
|
|
|
112
115
|
unpersisted = null;
|
|
113
116
|
if (entry) storage.writeDocument(entry.version, entry.document).catch(() => void 0);
|
|
114
117
|
}
|
|
115
|
-
function adoptRead(result, readPlaybackRevision) {
|
|
116
|
-
const
|
|
117
|
-
board: result.board,
|
|
118
|
-
items: result.items ?? {},
|
|
119
|
-
animations: result.animations ?? {}
|
|
120
|
-
});
|
|
121
|
-
if (!parsed.ok) throw new Error(`Board ${result.id} could not be read.`);
|
|
118
|
+
function adoptRead(result, base, readPlaybackRevision) {
|
|
119
|
+
const next = readBoardDocument(result, result.changes ? base : server);
|
|
122
120
|
title = result.title;
|
|
123
121
|
if (readPlaybackRevision === playbackRevision) playback = result.playback;
|
|
124
122
|
loaded = true;
|
|
125
|
-
const next = shareBoardDocument(server, parsed.document);
|
|
126
123
|
if (next === server && result.version === version) return;
|
|
127
124
|
adoptServer(next, result.version, true);
|
|
128
125
|
}
|
|
129
126
|
function refresh() {
|
|
127
|
+
if (disposed) return Promise.resolve();
|
|
130
128
|
if (refreshing) {
|
|
131
129
|
refreshAgain = true;
|
|
132
130
|
return refreshing;
|
|
@@ -136,9 +134,10 @@ function createBoardReplica(options) {
|
|
|
136
134
|
do {
|
|
137
135
|
refreshAgain = false;
|
|
138
136
|
const readPlaybackRevision = playbackRevision;
|
|
139
|
-
const
|
|
137
|
+
const base = server;
|
|
138
|
+
const result = await remote.get(base ? { since: version } : void 0);
|
|
140
139
|
if (disposed) return;
|
|
141
|
-
if (result.version >= version || !loaded) adoptRead(result, readPlaybackRevision);
|
|
140
|
+
if (result.version >= version || !loaded) adoptRead(result, base, readPlaybackRevision);
|
|
142
141
|
readError = null;
|
|
143
142
|
emit();
|
|
144
143
|
} while (refreshAgain && !disposed);
|
|
@@ -162,6 +161,7 @@ function createBoardReplica(options) {
|
|
|
162
161
|
async function flush() {
|
|
163
162
|
if (sending || disposed || !loaded || !restored) return;
|
|
164
163
|
sending = true;
|
|
164
|
+
let latest = 0;
|
|
165
165
|
try {
|
|
166
166
|
while (pending.length && !disposed) {
|
|
167
167
|
const entry = pending[0];
|
|
@@ -173,12 +173,12 @@ function createBoardReplica(options) {
|
|
|
173
173
|
const stillPending = pending.some((item) => item.mutationId === entry.mutationId);
|
|
174
174
|
pending = pending.filter((item) => item.mutationId !== entry.mutationId);
|
|
175
175
|
storage.deletePending(entry.mutationId).catch(() => void 0);
|
|
176
|
-
if (stillPending &&
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
176
|
+
if (stillPending && result.status === "applied" && result.version > version) sent = [...sent, {
|
|
177
|
+
version: result.version,
|
|
178
|
+
patch: entry.patch
|
|
179
|
+
}];
|
|
180
|
+
latest = Math.max(latest, result.version);
|
|
181
|
+
recompute();
|
|
182
182
|
retryDelay = RETRY_BASE_MS;
|
|
183
183
|
writeError = null;
|
|
184
184
|
emit();
|
|
@@ -198,6 +198,7 @@ function createBoardReplica(options) {
|
|
|
198
198
|
return;
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
|
+
if (latest > version && !disposed) refresh();
|
|
201
202
|
} finally {
|
|
202
203
|
sending = false;
|
|
203
204
|
}
|
|
@@ -220,7 +221,7 @@ function createBoardReplica(options) {
|
|
|
220
221
|
}
|
|
221
222
|
emit();
|
|
222
223
|
}
|
|
223
|
-
function retry(
|
|
224
|
+
function retry() {
|
|
224
225
|
if (disposed) return Promise.resolve();
|
|
225
226
|
if (retryTimer) clearTimeout(retryTimer);
|
|
226
227
|
retryTimer = null;
|
|
@@ -228,31 +229,24 @@ function createBoardReplica(options) {
|
|
|
228
229
|
readError = null;
|
|
229
230
|
writeError = null;
|
|
230
231
|
emit();
|
|
231
|
-
return
|
|
232
|
-
}
|
|
233
|
-
async function resync() {
|
|
234
|
-
await refreshing;
|
|
235
|
-
if (disposed) return;
|
|
236
|
-
if (!loaded || readError) return retry();
|
|
237
|
-
const revision = playbackRevision;
|
|
238
|
-
const head = await remote.get({ only: [] }).catch(() => null);
|
|
239
|
-
if (disposed) return;
|
|
240
|
-
if (!head || head.version > version) return retry();
|
|
241
|
-
if (revision === playbackRevision) playback = head.playback;
|
|
242
|
-
return retry(false);
|
|
232
|
+
return refresh().then(flush);
|
|
243
233
|
}
|
|
244
234
|
function onOnline() {
|
|
245
235
|
if (pending.length || readError || writeError) retry();
|
|
246
236
|
}
|
|
247
|
-
async function
|
|
248
|
-
const
|
|
237
|
+
async function restoreDocument() {
|
|
238
|
+
const cached = await storage.readDocument().catch(() => null);
|
|
239
|
+
if (disposed || server || !cached) return;
|
|
240
|
+
const parsed = parseBoardDocument(cached.document);
|
|
241
|
+
if (parsed.ok) adoptServer(parsed.document, cached.version, false);
|
|
242
|
+
}
|
|
243
|
+
async function restore(document) {
|
|
244
|
+
const [, stored] = await Promise.all([document, storage.listPending().catch(() => [])]);
|
|
249
245
|
if (disposed) return;
|
|
250
246
|
const known = new Set(stored.map((entry) => entry.mutationId));
|
|
251
247
|
pending = [...stored, ...pending.filter((entry) => !known.has(entry.mutationId))];
|
|
252
248
|
restored = true;
|
|
253
|
-
|
|
254
|
-
if (cached && parsed?.ok) adoptServer(parsed.document, cached.version, false);
|
|
255
|
-
else recompute();
|
|
249
|
+
recompute();
|
|
256
250
|
emit();
|
|
257
251
|
}
|
|
258
252
|
function receivePlayback(next) {
|
|
@@ -271,7 +265,12 @@ function createBoardReplica(options) {
|
|
|
271
265
|
async start() {
|
|
272
266
|
unsubscribeRemote ??= subscribeRemote();
|
|
273
267
|
globalThis.addEventListener?.("online", onOnline);
|
|
274
|
-
|
|
268
|
+
const document = restoreDocument();
|
|
269
|
+
const cacheWait = new Promise((resolve) => {
|
|
270
|
+
const timer = setTimeout(resolve, CACHE_WAIT_MS);
|
|
271
|
+
document.then(() => clearTimeout(timer));
|
|
272
|
+
});
|
|
273
|
+
await Promise.all([restore(document), Promise.race([document, cacheWait]).then(refresh)]);
|
|
275
274
|
await flush();
|
|
276
275
|
},
|
|
277
276
|
apply(patch) {
|
|
@@ -287,7 +286,7 @@ function createBoardReplica(options) {
|
|
|
287
286
|
receive,
|
|
288
287
|
receivePlayback,
|
|
289
288
|
refresh,
|
|
290
|
-
retry
|
|
289
|
+
retry,
|
|
291
290
|
dispose() {
|
|
292
291
|
disposed = true;
|
|
293
292
|
globalThis.removeEventListener?.("online", onOnline);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ModelFormat } from "../../protocol/dist/fs/mime.js";
|
|
2
2
|
import { BoardSceneItem } from "../model/scene.js";
|
|
3
3
|
import { BoardAssetMedia, BoardAssetRef } from "../model/asset-key.js";
|
|
4
|
-
import { BoardAssetSource
|
|
4
|
+
import { BoardAssetSource } from "../model/media-playback.js";
|
|
5
5
|
import "../model/index.js";
|
|
6
6
|
import { LruBudget } from "./lru.js";
|
|
7
7
|
import { BoardImageTexturePool } from "./texture-pool.js";
|
|
@@ -60,7 +60,7 @@ type BoardAssetPrefetch = {
|
|
|
60
60
|
type BoardAssetChange = "texture" | "tone";
|
|
61
61
|
type BoardAssetManagerOptions = {
|
|
62
62
|
/** Resolve a Space file path to a URL a texture can load from; null when unavailable. */
|
|
63
|
-
|
|
63
|
+
resolveFile: BoardAssetSource["resolveFile"];
|
|
64
64
|
subscribe?: BoardAssetSource["subscribe"];
|
|
65
65
|
/** undefined keeps the built-in rule, null shows none; cached until `invalidateItemAssets`. */
|
|
66
66
|
itemAsset?: (item: BoardSceneItem) => BoardAssetRef | null | undefined;
|
|
@@ -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,12 +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();
|
|
83
114
|
const keysByPath = /* @__PURE__ */ new Map();
|
|
84
115
|
const pathVersions = /* @__PURE__ */ new Map();
|
|
85
116
|
const starters = /* @__PURE__ */ new Map();
|
|
86
|
-
const
|
|
117
|
+
const unavailable = /* @__PURE__ */ new Set();
|
|
87
118
|
const queue = [];
|
|
88
119
|
const listeners = /* @__PURE__ */ new Set();
|
|
89
120
|
let active = 0;
|
|
@@ -139,7 +170,7 @@ function createBoardAssetManager(options) {
|
|
|
139
170
|
const itemEpoch = itemEpochs.get(item.id) ?? 0;
|
|
140
171
|
let record = keys.get(item);
|
|
141
172
|
if (record?.epoch !== keyEpoch || record.itemEpoch !== itemEpoch) {
|
|
142
|
-
const base =
|
|
173
|
+
const { key: base, natural } = resolveAsset(item);
|
|
143
174
|
const source = base ? boardAssetKeySource(base) : null;
|
|
144
175
|
const path = source?.kind === "file" ? source.value : null;
|
|
145
176
|
record = {
|
|
@@ -148,7 +179,8 @@ function createBoardAssetManager(options) {
|
|
|
148
179
|
base,
|
|
149
180
|
path,
|
|
150
181
|
version: 0,
|
|
151
|
-
key: base
|
|
182
|
+
key: base,
|
|
183
|
+
natural
|
|
152
184
|
};
|
|
153
185
|
keys.set(item, record);
|
|
154
186
|
}
|
|
@@ -161,14 +193,23 @@ function createBoardAssetManager(options) {
|
|
|
161
193
|
}
|
|
162
194
|
return record.key;
|
|
163
195
|
}
|
|
164
|
-
function
|
|
196
|
+
function resolveAsset(item) {
|
|
165
197
|
const asset = options.itemAsset?.(item);
|
|
198
|
+
const natural = (asset === void 0 ? boardMediaNaturalSize(item) : asset?.natural) ?? null;
|
|
166
199
|
const key = asset === void 0 ? boardAssetKey(item) : asset && boardAssetRefKey(asset);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
};
|
|
172
213
|
}
|
|
173
214
|
function clearRetry(entry) {
|
|
174
215
|
if (entry.retryTimer) {
|
|
@@ -196,6 +237,7 @@ function createBoardAssetManager(options) {
|
|
|
196
237
|
loading: false,
|
|
197
238
|
error: false,
|
|
198
239
|
attempts: 0,
|
|
240
|
+
frameless: false,
|
|
199
241
|
retryAt: 0,
|
|
200
242
|
retryTimer: null,
|
|
201
243
|
lastUsedAt: now()
|
|
@@ -242,8 +284,8 @@ function createBoardAssetManager(options) {
|
|
|
242
284
|
if (!disposed) trim();
|
|
243
285
|
}, TRIM_DELAY);
|
|
244
286
|
}
|
|
245
|
-
function shrunkFootprint({ media, texture }) {
|
|
246
|
-
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;
|
|
247
289
|
const scale = SHRINK_EDGE / Math.max(texture.width, texture.height);
|
|
248
290
|
return scale < 1 ? Math.round(texture.width * scale) * Math.round(texture.height * scale) * 4 : void 0;
|
|
249
291
|
}
|
|
@@ -303,7 +345,9 @@ function createBoardAssetManager(options) {
|
|
|
303
345
|
entry.texture = texture;
|
|
304
346
|
entry.url = loaded.url;
|
|
305
347
|
entry.edge = loaded.edge;
|
|
306
|
-
|
|
348
|
+
const decoded = videoTextureNaturalSize(texture);
|
|
349
|
+
if (decoded) entry.natural = decoded;
|
|
350
|
+
else if (loaded.edge === Number.POSITIVE_INFINITY && !variantSource(loaded.url)) entry.natural = {
|
|
307
351
|
width: texture.width,
|
|
308
352
|
height: texture.height
|
|
309
353
|
};
|
|
@@ -320,23 +364,27 @@ function createBoardAssetManager(options) {
|
|
|
320
364
|
notify(key);
|
|
321
365
|
return;
|
|
322
366
|
}
|
|
323
|
-
|
|
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;
|
|
324
369
|
entry.error = true;
|
|
325
370
|
entry.attempts += 1;
|
|
326
371
|
entry.retryAt = now() + Math.min(1e3 * 2 ** entry.attempts, MAX_RETRY_DELAY);
|
|
327
|
-
console.warn(`[board] failed to load preview for ${key}`, error);
|
|
328
|
-
if (
|
|
372
|
+
if (error !== NO_PREVIEW) console.warn(`[board] failed to load preview for ${key}`, error);
|
|
373
|
+
if (final) unavailable.add(key);
|
|
329
374
|
if (entry.refs <= 0) evict(key, entry);
|
|
330
375
|
else {
|
|
331
|
-
if (
|
|
376
|
+
if (!final) scheduleRetry(key, entry);
|
|
332
377
|
notify(key);
|
|
333
378
|
}
|
|
334
379
|
}
|
|
335
380
|
function refinable(key, entry) {
|
|
336
|
-
|
|
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;
|
|
337
383
|
}
|
|
338
384
|
function refine(key, entry) {
|
|
339
|
-
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);
|
|
340
388
|
}
|
|
341
389
|
function enqueue(key, media, load) {
|
|
342
390
|
if (queue.some((task) => task.key === key)) return;
|
|
@@ -390,55 +438,79 @@ function createBoardAssetManager(options) {
|
|
|
390
438
|
task.load(entry).then((loaded) => done(loaded), (error) => done(null, error));
|
|
391
439
|
}
|
|
392
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
|
+
}
|
|
393
467
|
function textureLoad(media, source) {
|
|
394
468
|
return async (entry) => {
|
|
395
469
|
entry.source = source;
|
|
396
470
|
const { cover, want: edge } = entry;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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);
|
|
404
490
|
if (fitted) return {
|
|
405
491
|
texture: fitted,
|
|
406
492
|
url: "",
|
|
407
493
|
edge: sharpEdge(fitted, edge, true, cover)
|
|
408
494
|
};
|
|
409
495
|
}
|
|
410
|
-
|
|
411
|
-
if (!texture) return null;
|
|
412
|
-
const bound = boundEdge(texture, cover);
|
|
413
|
-
if (media === "image" && shrinkTexture && edge < bound / 2) {
|
|
414
|
-
const longEdge = Math.round(edge * Math.max(texture.width, texture.height) / bound);
|
|
415
|
-
const small = await shrinkTexture(texture, longEdge).catch(() => null);
|
|
416
|
-
if (small) {
|
|
417
|
-
releaseTexture(media, url, texture);
|
|
418
|
-
return {
|
|
419
|
-
texture: small,
|
|
420
|
-
url: "",
|
|
421
|
-
edge
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
return {
|
|
426
|
-
texture,
|
|
427
|
-
url,
|
|
428
|
-
edge: sharpEdge(texture, edge, Boolean(variant), cover)
|
|
429
|
-
};
|
|
496
|
+
return loadSized(entry, variant ?? source, "image", Boolean(variant));
|
|
430
497
|
};
|
|
431
498
|
}
|
|
432
|
-
function
|
|
499
|
+
function startFile(key, media, path) {
|
|
433
500
|
return (entry) => {
|
|
434
501
|
const fresh = entry.attempts > 0;
|
|
435
502
|
entry.loading = true;
|
|
436
|
-
|
|
503
|
+
options.resolveFile(path, fresh ? { fresh } : void 0).then((file) => {
|
|
437
504
|
if (disposed || entries.get(key) !== entry) return;
|
|
438
505
|
if (entry.refs <= 0 && !warm.has(key)) return evict(key, entry);
|
|
439
|
-
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
|
+
};
|
|
440
512
|
entry.loading = false;
|
|
441
|
-
enqueue(key, media, textureLoad(media, url));
|
|
513
|
+
enqueue(key, slotOf(media, url, entry.frameless), textureLoad(media, url));
|
|
442
514
|
}, (error) => {
|
|
443
515
|
if (!disposed) adopt(key, entry, null, error);
|
|
444
516
|
});
|
|
@@ -446,12 +518,10 @@ function createBoardAssetManager(options) {
|
|
|
446
518
|
}
|
|
447
519
|
function probeNaturalSize(key, entry, source) {
|
|
448
520
|
entry.natural = null;
|
|
449
|
-
|
|
450
|
-
if (disposed || entries.get(key) !== entry || !
|
|
451
|
-
entry.natural =
|
|
452
|
-
|
|
453
|
-
height
|
|
454
|
-
};
|
|
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);
|
|
455
525
|
notify(key);
|
|
456
526
|
});
|
|
457
527
|
}
|
|
@@ -485,9 +555,9 @@ function createBoardAssetManager(options) {
|
|
|
485
555
|
return load ? () => enqueue(key, "model", load) : null;
|
|
486
556
|
}
|
|
487
557
|
const media = source.media;
|
|
488
|
-
if (source.kind === "file") return
|
|
558
|
+
if (source.kind === "file") return startFile(key, media, source.value);
|
|
489
559
|
const load = textureLoad(media, source.value);
|
|
490
|
-
return () => enqueue(key, media, load);
|
|
560
|
+
return (entry) => enqueue(key, slotOf(media, source.value, entry.frameless), load);
|
|
491
561
|
}
|
|
492
562
|
const manager = {
|
|
493
563
|
assetKey: managedAssetKey,
|
|
@@ -509,11 +579,12 @@ function createBoardAssetManager(options) {
|
|
|
509
579
|
requestItem(item, edge = Number.POSITIVE_INFINITY) {
|
|
510
580
|
if (disposed) return;
|
|
511
581
|
const key = managedAssetKey(item);
|
|
512
|
-
if (!key ||
|
|
582
|
+
if (!key || unavailable.has(key)) return;
|
|
513
583
|
const source = boardAssetKeySource(key);
|
|
514
584
|
if (!source) return;
|
|
515
585
|
const entry = ensureEntry(key);
|
|
516
586
|
entry.lastUsedAt = now();
|
|
587
|
+
if (!entry.natural) entry.natural = keys.get(item)?.natural ?? entry.natural;
|
|
517
588
|
entry.center = {
|
|
518
589
|
x: item.frame.x + item.frame.width / 2,
|
|
519
590
|
y: item.frame.y + item.frame.height / 2
|
|
@@ -701,7 +772,7 @@ function createBoardAssetManager(options) {
|
|
|
701
772
|
pathVersions.clear();
|
|
702
773
|
itemEpochs.clear();
|
|
703
774
|
starters.clear();
|
|
704
|
-
|
|
775
|
+
unavailable.clear();
|
|
705
776
|
for (const [key, entry] of entries) evict(key, entry);
|
|
706
777
|
entries.clear();
|
|
707
778
|
for (const release of [...retiring]) release();
|