@neta-art/cohub 12.1.0 → 12.2.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 +8 -5
- package/dist/board/editor/editor.d.ts +1 -0
- package/dist/board/editor/editor.js +4 -0
- package/dist/board/editor/snapshots.js +5 -57
- package/dist/board/export/index.d.ts +2 -2
- package/dist/board/export/node/fonts.js +4 -0
- package/dist/board/export/node/index.d.ts +3 -0
- package/dist/board/export/node/index.js +13 -1
- package/dist/board/export/scene.d.ts +2 -2
- package/dist/board/export/scene.js +2 -2
- package/dist/board/model/asset-key.d.ts +16 -0
- package/dist/board/model/asset-key.js +92 -0
- package/dist/board/model/export-assets.d.ts +7 -3
- package/dist/board/model/export-assets.js +34 -13
- package/dist/board/model/index.d.ts +7 -5
- package/dist/board/model/index.js +9 -7
- package/dist/board/model/items/create.js +1 -1
- package/dist/board/model/media-sizes.d.ts +28 -0
- package/dist/board/model/media-sizes.js +64 -0
- package/dist/board/model/palette.d.ts +1 -0
- package/dist/board/model/palette.js +20 -130
- package/dist/board/model/text-metrics.d.ts +8 -3
- package/dist/board/model/text-metrics.js +13 -2
- package/dist/board/model/themes.d.ts +488 -0
- package/dist/board/model/themes.js +495 -0
- package/dist/board/render/card-tag.js +2 -2
- package/dist/board/render/index.d.ts +6 -4
- package/dist/board/render/index.js +5 -3
- package/dist/board/render/lod.d.ts +22 -0
- package/dist/board/render/lod.js +27 -0
- package/dist/board/render/media-interaction.js +1 -1
- package/dist/board/render/palette.js +2 -25
- package/dist/board/render/renderers/audio-card-renderer.js +2 -1
- package/dist/board/render/renderers/board-renderer-registry.d.ts +10 -0
- package/dist/board/render/renderers/custom-card-renderer.js +5 -5
- package/dist/board/render/renderers/far-plate.d.ts +11 -1
- package/dist/board/render/renderers/far-plate.js +27 -8
- package/dist/board/render/renderers/file-card-renderer.js +3 -5
- package/dist/board/render/renderers/image-card-renderer.js +32 -6
- package/dist/board/render/renderers/model-card-renderer.js +1 -1
- package/dist/board/render/renderers/shape-card-renderer.js +25 -24
- package/dist/board/render/renderers/task-card-renderer.js +7 -8
- package/dist/board/render/renderers/text-card-renderer.js +86 -41
- package/dist/board/render/renderers/video-card-renderer.js +2 -1
- package/dist/board/render/style.d.ts +7 -2
- package/dist/board/render/style.js +5 -1
- package/dist/board/render/surface.d.ts +5 -0
- package/dist/board/render/surface.js +13 -3
- package/dist/board/render/text-fit.js +8 -12
- package/dist/board/render/text-measurement.d.ts +15 -1
- package/dist/board/render/text-measurement.js +113 -20
- package/dist/board/render/text-resolution.d.ts +1 -6
- package/dist/board/render/text-resolution.js +1 -10
- package/dist/board/render/theme.d.ts +15 -0
- package/dist/board/render/theme.js +15 -0
- package/dist/board/stage/assets.d.ts +15 -4
- package/dist/board/stage/assets.js +155 -89
- package/dist/board/stage/index.d.ts +5 -5
- package/dist/board/stage/index.js +4 -4
- package/dist/board/stage/lru.d.ts +13 -9
- package/dist/board/stage/lru.js +38 -20
- package/dist/board/stage/scene.d.ts +11 -1
- package/dist/board/stage/scene.js +213 -138
- package/dist/board/stage/stage.d.ts +12 -1
- package/dist/board/stage/stage.js +97 -24
- package/dist/board/stage/texture-pool.d.ts +8 -1
- package/dist/board/stage/texture-pool.js +37 -1
- package/dist/chunks/app-realtime.js +1 -1
- package/dist/chunks/{transport.js → http-error.js} +37 -1
- package/dist/chunks/http.js +30 -10
- package/dist/http.js +1 -1
- package/dist/index.js +139 -131
- package/dist/media.d.ts +3 -27
- package/dist/media.js +3 -82
- package/dist/protocol/dist/board-constants.js +3 -3
- package/dist/protocol/dist/board-layout.d.ts +17 -3
- package/dist/protocol/dist/board-layout.js +68 -11
- package/dist/protocol/dist/core/media.d.ts +35 -1
- package/dist/protocol/dist/core/media.js +123 -1
- package/dist/protocol/dist/index.d.ts +2 -2
- package/dist/websocket.js +951 -1
- package/package.json +3 -2
- package/dist/board/model/image-key.d.ts +0 -9
- package/dist/board/model/image-key.js +0 -33
- package/dist/chunks/websocket.js +0 -988
|
@@ -1,81 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { boardImageKeySource, imageAssetKey } from "../model/image-key.js";
|
|
1
|
+
import { probeMediaInfo } from "../../protocol/dist/core/media.js";
|
|
2
|
+
import { MEDIA_VARIANT_SIZES, imageVariantUrl, variantSource } from "../../media.js";
|
|
3
|
+
import { boardModelFormat } from "../model/media.js";
|
|
4
|
+
import { boardAssetKey, boardAssetKeySource } from "../model/asset-key.js";
|
|
6
5
|
import { loadVideoThumbnailTexture, videoTextureNaturalSize } from "../render/video-thumbnail.js";
|
|
7
|
-
import { DEFAULT_LRU_BUDGET,
|
|
8
|
-
import { boardImageTexturePool, loadBoardImageTexture, unloadBoardImageTexture } from "./texture-pool.js";
|
|
6
|
+
import { DEFAULT_LRU_BUDGET, planLruTrim } from "./lru.js";
|
|
7
|
+
import { boardImageTexturePool, loadBoardImageTexture, loadFittedImageTexture, shrinkBoardTexture, unloadBoardImageTexture } from "./texture-pool.js";
|
|
9
8
|
import { Texture } from "pixi.js";
|
|
10
9
|
//#region src/board/stage/assets.ts
|
|
11
|
-
function taskPreviewKey(artifact) {
|
|
12
|
-
const cover = taskArtifactPreviewUrl(artifact);
|
|
13
|
-
if (cover) return `url:${cover}`;
|
|
14
|
-
if (artifact?.type !== "video") return null;
|
|
15
|
-
const frame = videoFrameUrl(artifact.url, 768);
|
|
16
|
-
return frame ? `url:${frame}` : `video-url:${encodeURIComponent(artifact.url)}`;
|
|
17
|
-
}
|
|
18
|
-
const LARGEST_VARIANT = MEDIA_VARIANT_SIZES[MEDIA_VARIANT_SIZES.length - 1];
|
|
19
|
-
function boardTextureEdge(item, scale) {
|
|
20
|
-
const crop = item.type === "image" ? item.props.crop : void 0;
|
|
21
|
-
const pixels = Math.max(item.frame.width / (crop?.w || 1), item.frame.height / (crop?.h || 1)) * scale;
|
|
22
|
-
return pixels > LARGEST_VARIANT ? Number.POSITIVE_INFINITY : mediaVariantSize(pixels);
|
|
23
|
-
}
|
|
24
10
|
function variantUrl(source, edge) {
|
|
25
|
-
if (!(edge <=
|
|
11
|
+
if (!(edge <= 1536)) return null;
|
|
26
12
|
const url = imageVariantUrl(source, edge);
|
|
27
13
|
return url && url !== source ? url : null;
|
|
28
14
|
}
|
|
29
|
-
const
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
if (!source) return null;
|
|
33
|
-
return source.kind === "url" ? `${media}-url:${encodeURIComponent(source.value)}` : `${media}:${encodeURIComponent(source.value)}:${mtimeMs ?? "unknown"}`;
|
|
34
|
-
}
|
|
35
|
-
function boardAssetKey(item) {
|
|
36
|
-
if (item.type === "task") return taskPreviewKey(featuredTaskArtifact(item.props.snapshot.artifacts));
|
|
37
|
-
if (item.type === "video") return stillKey("video", item.props.src, item.props.snapshot?.mtimeMs);
|
|
38
|
-
if (item.type === "file" && isBoardModelItem(item)) return stillKey("model", item.props.src, item.props.snapshot?.mtimeMs);
|
|
39
|
-
return imageAssetKey(item);
|
|
40
|
-
}
|
|
41
|
-
function keySource(key) {
|
|
42
|
-
let baseKey = key;
|
|
43
|
-
if (key.startsWith("local:")) {
|
|
44
|
-
const separator = key.indexOf(":", 6);
|
|
45
|
-
if (separator < 0) return null;
|
|
46
|
-
baseKey = key.slice(separator + 1);
|
|
47
|
-
}
|
|
48
|
-
for (const media of STILL_MEDIA) {
|
|
49
|
-
if (baseKey.startsWith(`${media}-url:`)) try {
|
|
50
|
-
return {
|
|
51
|
-
kind: "url",
|
|
52
|
-
media,
|
|
53
|
-
value: decodeURIComponent(baseKey.slice(media.length + 5))
|
|
54
|
-
};
|
|
55
|
-
} catch {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
if (baseKey.startsWith(`${media}:`)) {
|
|
59
|
-
const separator = baseKey.lastIndexOf(":");
|
|
60
|
-
if (separator <= media.length) return null;
|
|
61
|
-
try {
|
|
62
|
-
return {
|
|
63
|
-
kind: "file",
|
|
64
|
-
media,
|
|
65
|
-
value: decodeURIComponent(baseKey.slice(media.length + 1, separator))
|
|
66
|
-
};
|
|
67
|
-
} catch {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
const image = boardImageKeySource(baseKey);
|
|
73
|
-
return image ? {
|
|
74
|
-
...image,
|
|
75
|
-
media: "image"
|
|
76
|
-
} : null;
|
|
15
|
+
const SMALLEST_VARIANT = MEDIA_VARIANT_SIZES[0];
|
|
16
|
+
function sharpEdge(texture, edge, resized) {
|
|
17
|
+
return resized && Math.max(texture.width, texture.height) >= edge ? edge : Number.POSITIVE_INFINITY;
|
|
77
18
|
}
|
|
78
19
|
const MAX_RETRY_DELAY = 3e4;
|
|
20
|
+
const SHRINK_EDGE = 256;
|
|
21
|
+
const SHRINK_CONCURRENCY = 4;
|
|
22
|
+
const TRIM_DELAY = 400;
|
|
23
|
+
const TRIM_MAX_DELAY = 2e3;
|
|
79
24
|
function footprintOf(texture) {
|
|
80
25
|
if (!texture) return 0;
|
|
81
26
|
return (texture.width || 0) * (texture.height || 0) * 4;
|
|
@@ -92,6 +37,7 @@ function createBoardAssetManager(options) {
|
|
|
92
37
|
const imagePool = options.imageTexturePool ?? boardImageTexturePool;
|
|
93
38
|
const useSharedImagePool = !options.loadTexture && !options.unloadTexture;
|
|
94
39
|
const loadTexture = options.loadTexture ?? ((url, media) => media === "video" ? loadVideoThumbnailTexture(url) : useSharedImagePool ? imagePool.acquire(url) : loadBoardImageTexture(url));
|
|
40
|
+
const shrinkTexture = options.shrinkTexture ?? (useSharedImagePool ? shrinkBoardTexture : null);
|
|
95
41
|
const pendingUnloads = /* @__PURE__ */ new Map();
|
|
96
42
|
const unloadTexture = options.unloadTexture ?? (async (url, texture, media) => {
|
|
97
43
|
if (media !== "image" || !url) {
|
|
@@ -106,8 +52,7 @@ function createBoardAssetManager(options) {
|
|
|
106
52
|
}
|
|
107
53
|
await unloadBoardImageTexture(url, texture);
|
|
108
54
|
});
|
|
109
|
-
function releaseTexture(
|
|
110
|
-
const media = keySource(key)?.media ?? "image";
|
|
55
|
+
function releaseTexture(media, url, texture) {
|
|
111
56
|
const pendingKey = `${media}:${url}`;
|
|
112
57
|
const promise = unloadTexture(url, texture, media).catch(() => {}).then(() => {
|
|
113
58
|
if (url && pendingUnloads.get(pendingKey) === promise) pendingUnloads.delete(pendingKey);
|
|
@@ -115,9 +60,9 @@ function createBoardAssetManager(options) {
|
|
|
115
60
|
if (url) pendingUnloads.set(pendingKey, promise);
|
|
116
61
|
}
|
|
117
62
|
const retiring = /* @__PURE__ */ new Set();
|
|
118
|
-
function retire(
|
|
63
|
+
function retire(media, url, texture) {
|
|
119
64
|
const release = () => {
|
|
120
|
-
if (retiring.delete(release)) releaseTexture(
|
|
65
|
+
if (retiring.delete(release)) releaseTexture(media, url, texture);
|
|
121
66
|
};
|
|
122
67
|
retiring.add(release);
|
|
123
68
|
if (typeof requestAnimationFrame === "function") requestAnimationFrame(release);
|
|
@@ -137,13 +82,29 @@ function createBoardAssetManager(options) {
|
|
|
137
82
|
model: 0
|
|
138
83
|
};
|
|
139
84
|
let disposed = false;
|
|
85
|
+
let focus = null;
|
|
86
|
+
let trimTimer = null;
|
|
87
|
+
let trimSince = 0;
|
|
88
|
+
let shrinking = 0;
|
|
140
89
|
function notify(key) {
|
|
141
90
|
for (const listener of listeners) listener(key);
|
|
142
91
|
}
|
|
92
|
+
const keys = /* @__PURE__ */ new WeakMap();
|
|
93
|
+
let keyEpoch = 0;
|
|
143
94
|
function managedAssetKey(item) {
|
|
95
|
+
const cached = keys.get(item);
|
|
96
|
+
if (cached?.epoch === keyEpoch) return cached.key;
|
|
97
|
+
const key = resolveAssetKey(item);
|
|
98
|
+
keys.set(item, {
|
|
99
|
+
epoch: keyEpoch,
|
|
100
|
+
key
|
|
101
|
+
});
|
|
102
|
+
return key;
|
|
103
|
+
}
|
|
104
|
+
function resolveAssetKey(item) {
|
|
144
105
|
const key = boardAssetKey(item);
|
|
145
106
|
if (!key) return null;
|
|
146
|
-
const source =
|
|
107
|
+
const source = boardAssetKeySource(key);
|
|
147
108
|
if (source?.media === "video" && options.loadVideoPreviews === false) return null;
|
|
148
109
|
if (source?.media === "model" && !modelPreviewable(item)) return null;
|
|
149
110
|
if (source?.kind !== "file") return key;
|
|
@@ -160,12 +121,13 @@ function createBoardAssetManager(options) {
|
|
|
160
121
|
let entry = entries.get(key);
|
|
161
122
|
if (!entry) {
|
|
162
123
|
entry = {
|
|
163
|
-
media:
|
|
124
|
+
media: boardAssetKeySource(key)?.media ?? "image",
|
|
164
125
|
source: null,
|
|
165
126
|
url: null,
|
|
166
127
|
texture: null,
|
|
167
128
|
edge: 0,
|
|
168
129
|
want: 0,
|
|
130
|
+
center: null,
|
|
169
131
|
natural: void 0,
|
|
170
132
|
refs: 0,
|
|
171
133
|
loading: false,
|
|
@@ -195,26 +157,68 @@ function createBoardAssetManager(options) {
|
|
|
195
157
|
function evict(key, entry) {
|
|
196
158
|
clearRetry(entry);
|
|
197
159
|
starters.delete(key);
|
|
198
|
-
if (entry.url || entry.texture) releaseTexture(
|
|
160
|
+
if (entry.url || entry.texture) releaseTexture(entry.media, entry.url ?? "", entry.texture);
|
|
199
161
|
entries.delete(key);
|
|
200
162
|
}
|
|
163
|
+
function scheduleTrim() {
|
|
164
|
+
const at = now();
|
|
165
|
+
if (trimTimer === null) trimSince = at;
|
|
166
|
+
else if (at - trimSince >= TRIM_MAX_DELAY) return;
|
|
167
|
+
else clearTimeout(trimTimer);
|
|
168
|
+
trimTimer = setTimeout(() => {
|
|
169
|
+
trimTimer = null;
|
|
170
|
+
if (!disposed) trim();
|
|
171
|
+
}, TRIM_DELAY);
|
|
172
|
+
}
|
|
173
|
+
function shrunkFootprint({ media, texture }) {
|
|
174
|
+
if (!shrinkTexture || media !== "image" || !texture) return void 0;
|
|
175
|
+
const scale = SHRINK_EDGE / Math.max(texture.width, texture.height);
|
|
176
|
+
return scale < 1 ? Math.round(texture.width * scale) * Math.round(texture.height * scale) * 4 : void 0;
|
|
177
|
+
}
|
|
201
178
|
function trim() {
|
|
202
179
|
const cooling = [];
|
|
203
180
|
for (const [key, entry] of entries) if (entry.refs <= 0 && entry.texture && !entry.loading) cooling.push({
|
|
204
181
|
key,
|
|
205
182
|
lastUsedAt: entry.lastUsedAt,
|
|
206
|
-
bytes: footprintOf(entry.texture)
|
|
183
|
+
bytes: footprintOf(entry.texture),
|
|
184
|
+
shrunkBytes: shrunkFootprint(entry)
|
|
207
185
|
});
|
|
208
|
-
const
|
|
209
|
-
for (const key of
|
|
186
|
+
const plan = planLruTrim(cooling, budget);
|
|
187
|
+
for (const key of plan.evict) {
|
|
210
188
|
const entry = entries.get(key);
|
|
211
189
|
if (entry) evict(key, entry);
|
|
212
190
|
}
|
|
191
|
+
for (const key of plan.shrink.slice(0, Math.max(0, SHRINK_CONCURRENCY - shrinking))) {
|
|
192
|
+
const entry = entries.get(key);
|
|
193
|
+
if (entry) shrink(key, entry);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function shrink(key, entry) {
|
|
197
|
+
const { texture, url } = entry;
|
|
198
|
+
if (!shrinkTexture || !texture) return;
|
|
199
|
+
entry.loading = true;
|
|
200
|
+
shrinking += 1;
|
|
201
|
+
shrinkTexture(texture, SHRINK_EDGE).catch(() => null).then((small) => {
|
|
202
|
+
shrinking -= 1;
|
|
203
|
+
entry.loading = false;
|
|
204
|
+
const current = entries.get(key) === entry;
|
|
205
|
+
if (small && current && entry.refs <= 0) {
|
|
206
|
+
entry.texture = small;
|
|
207
|
+
entry.url = "";
|
|
208
|
+
entry.edge = SHRINK_EDGE;
|
|
209
|
+
releaseTexture(entry.media, url ?? "", texture);
|
|
210
|
+
} else {
|
|
211
|
+
if (small) releaseTexture(entry.media, "", small);
|
|
212
|
+
if (current && entry.refs > 0) refine(key, entry);
|
|
213
|
+
else if (current) evict(key, entry);
|
|
214
|
+
}
|
|
215
|
+
if (!disposed) trim();
|
|
216
|
+
});
|
|
213
217
|
}
|
|
214
218
|
function adopt(key, entry, loaded, error) {
|
|
215
219
|
entry.loading = false;
|
|
216
220
|
if (entries.get(key) !== entry) {
|
|
217
|
-
if (loaded) releaseTexture(
|
|
221
|
+
if (loaded) releaseTexture(entry.media, loaded.url, loaded.texture);
|
|
218
222
|
return;
|
|
219
223
|
}
|
|
220
224
|
if (loaded) {
|
|
@@ -231,9 +235,9 @@ function createBoardAssetManager(options) {
|
|
|
231
235
|
height: texture.height
|
|
232
236
|
};
|
|
233
237
|
entry.lastUsedAt = now();
|
|
234
|
-
if (entry.refs <= 0)
|
|
238
|
+
if (entry.refs <= 0) scheduleTrim();
|
|
235
239
|
else notify(key);
|
|
236
|
-
if (replaced) retire(
|
|
240
|
+
if (replaced) retire(entry.media, replacedUrl, replaced);
|
|
237
241
|
refine(key, entry);
|
|
238
242
|
return;
|
|
239
243
|
}
|
|
@@ -269,9 +273,25 @@ function createBoardAssetManager(options) {
|
|
|
269
273
|
});
|
|
270
274
|
pump();
|
|
271
275
|
}
|
|
276
|
+
function nextTask() {
|
|
277
|
+
let best = -1;
|
|
278
|
+
let nearest = Number.POSITIVE_INFINITY;
|
|
279
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
280
|
+
const task = queue[index];
|
|
281
|
+
if (activeByMedia[task.media] >= limits[task.media]) continue;
|
|
282
|
+
const center = entries.get(task.key)?.center;
|
|
283
|
+
if (!focus || !center) return index;
|
|
284
|
+
const distance = (center.x - focus.x) ** 2 + (center.y - focus.y) ** 2;
|
|
285
|
+
if (distance < nearest) {
|
|
286
|
+
nearest = distance;
|
|
287
|
+
best = index;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return best;
|
|
291
|
+
}
|
|
272
292
|
function pump() {
|
|
273
293
|
while (!disposed && active < concurrency && queue.length > 0) {
|
|
274
|
-
const taskIndex =
|
|
294
|
+
const taskIndex = nextTask();
|
|
275
295
|
if (taskIndex < 0) return;
|
|
276
296
|
const [task] = queue.splice(taskIndex, 1);
|
|
277
297
|
if (!task) continue;
|
|
@@ -293,17 +313,36 @@ function createBoardAssetManager(options) {
|
|
|
293
313
|
return async (entry) => {
|
|
294
314
|
entry.source = source;
|
|
295
315
|
const edge = entry.want;
|
|
296
|
-
const variant = media === "image" ? variantUrl(source, edge) : null;
|
|
316
|
+
const variant = media === "image" ? variantUrl(source, Math.max(edge, SMALLEST_VARIANT)) : null;
|
|
297
317
|
const url = variant ?? source;
|
|
298
318
|
const pending = pendingUnloads.get(`${media}:${url}`);
|
|
299
319
|
if (pending) await pending;
|
|
300
320
|
if (disposed) return null;
|
|
321
|
+
if (media === "image" && !variant && useSharedImagePool && Number.isFinite(edge)) {
|
|
322
|
+
const fitted = await loadFittedImageTexture(url, edge).catch(() => null);
|
|
323
|
+
if (fitted) return {
|
|
324
|
+
texture: fitted,
|
|
325
|
+
url: "",
|
|
326
|
+
edge: sharpEdge(fitted, edge, true)
|
|
327
|
+
};
|
|
328
|
+
}
|
|
301
329
|
const texture = await loadTexture(url, media);
|
|
302
330
|
if (!texture) return null;
|
|
331
|
+
if (media === "image" && shrinkTexture && edge < Math.max(texture.width, texture.height) / 2) {
|
|
332
|
+
const small = await shrinkTexture(texture, edge).catch(() => null);
|
|
333
|
+
if (small) {
|
|
334
|
+
releaseTexture(media, url, texture);
|
|
335
|
+
return {
|
|
336
|
+
texture: small,
|
|
337
|
+
url: "",
|
|
338
|
+
edge
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
303
342
|
return {
|
|
304
343
|
texture,
|
|
305
344
|
url,
|
|
306
|
-
edge:
|
|
345
|
+
edge: sharpEdge(texture, edge, Boolean(variant))
|
|
307
346
|
};
|
|
308
347
|
};
|
|
309
348
|
}
|
|
@@ -372,21 +411,26 @@ function createBoardAssetManager(options) {
|
|
|
372
411
|
invalidatePath(path) {
|
|
373
412
|
if (disposed) return;
|
|
374
413
|
const stale = [...entries.keys()].filter((key) => {
|
|
375
|
-
const source =
|
|
414
|
+
const source = boardAssetKeySource(key);
|
|
376
415
|
return source?.kind === "file" && source.value === path;
|
|
377
416
|
});
|
|
378
417
|
if (stale.length === 0) return;
|
|
379
418
|
pathVersions.set(path, (pathVersions.get(path) ?? 0) + 1);
|
|
419
|
+
keyEpoch += 1;
|
|
380
420
|
for (const key of stale) notify(key);
|
|
381
421
|
},
|
|
382
422
|
requestItem(item, edge = Number.POSITIVE_INFINITY) {
|
|
383
423
|
if (disposed) return;
|
|
384
424
|
const key = managedAssetKey(item);
|
|
385
425
|
if (!key || failedModels.has(key)) return;
|
|
386
|
-
const source =
|
|
426
|
+
const source = boardAssetKeySource(key);
|
|
387
427
|
if (!source) return;
|
|
388
428
|
const entry = ensureEntry(key);
|
|
389
429
|
entry.lastUsedAt = now();
|
|
430
|
+
entry.center = {
|
|
431
|
+
x: item.frame.x + item.frame.width / 2,
|
|
432
|
+
y: item.frame.y + item.frame.height / 2
|
|
433
|
+
};
|
|
390
434
|
if (edge > entry.want) entry.want = edge;
|
|
391
435
|
if (entry.texture) return refine(key, entry);
|
|
392
436
|
if (entry.loading || entry.retryTimer) return;
|
|
@@ -413,8 +457,12 @@ function createBoardAssetManager(options) {
|
|
|
413
457
|
hasError(key) {
|
|
414
458
|
return entries.get(key)?.error ?? false;
|
|
415
459
|
},
|
|
460
|
+
focus(point) {
|
|
461
|
+
focus = point;
|
|
462
|
+
},
|
|
416
463
|
acquire(key) {
|
|
417
464
|
const entry = ensureEntry(key);
|
|
465
|
+
if (entry.refs <= 0) entry.want = 0;
|
|
418
466
|
entry.refs += 1;
|
|
419
467
|
entry.lastUsedAt = now();
|
|
420
468
|
},
|
|
@@ -427,7 +475,7 @@ function createBoardAssetManager(options) {
|
|
|
427
475
|
clearRetry(entry);
|
|
428
476
|
if (entry.loading) return;
|
|
429
477
|
if (!entry.texture) evict(key, entry);
|
|
430
|
-
else
|
|
478
|
+
else scheduleTrim();
|
|
431
479
|
},
|
|
432
480
|
async withTextures(items, use, loadOptions) {
|
|
433
481
|
const keys = /* @__PURE__ */ new Set();
|
|
@@ -467,6 +515,22 @@ function createBoardAssetManager(options) {
|
|
|
467
515
|
for (const key of keys) manager.release(key);
|
|
468
516
|
}
|
|
469
517
|
},
|
|
518
|
+
stats() {
|
|
519
|
+
let textures = 0;
|
|
520
|
+
let cooling = 0;
|
|
521
|
+
let bytes = 0;
|
|
522
|
+
for (const entry of entries.values()) {
|
|
523
|
+
if (!entry.texture) continue;
|
|
524
|
+
textures += 1;
|
|
525
|
+
if (entry.refs <= 0) cooling += 1;
|
|
526
|
+
bytes += footprintOf(entry.texture);
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
textures,
|
|
530
|
+
cooling,
|
|
531
|
+
bytes
|
|
532
|
+
};
|
|
533
|
+
},
|
|
470
534
|
subscribe(listener) {
|
|
471
535
|
listeners.add(listener);
|
|
472
536
|
return () => {
|
|
@@ -475,6 +539,8 @@ function createBoardAssetManager(options) {
|
|
|
475
539
|
},
|
|
476
540
|
destroy() {
|
|
477
541
|
disposed = true;
|
|
542
|
+
if (trimTimer !== null) clearTimeout(trimTimer);
|
|
543
|
+
trimTimer = null;
|
|
478
544
|
queue.length = 0;
|
|
479
545
|
listeners.clear();
|
|
480
546
|
pathVersions.clear();
|
|
@@ -488,4 +554,4 @@ function createBoardAssetManager(options) {
|
|
|
488
554
|
return manager;
|
|
489
555
|
}
|
|
490
556
|
//#endregion
|
|
491
|
-
export {
|
|
557
|
+
export { createBoardAssetManager };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, selectLruEvictions } from "./lru.js";
|
|
2
|
-
import { BoardImageTexturePool, boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, unloadBoardImageTexture } from "./texture-pool.js";
|
|
3
|
-
import { BoardAssetManager, BoardAssetManagerOptions, BoardModelPreviewRequest, BoardModelPreviews,
|
|
1
|
+
import { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, LruTrim, planLruTrim, selectLruEvictions } from "./lru.js";
|
|
2
|
+
import { BoardImageTexturePool, boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, loadFittedImageTexture, shrinkBoardTexture, unloadBoardImageTexture } from "./texture-pool.js";
|
|
3
|
+
import { BoardAssetManager, BoardAssetManagerOptions, BoardModelPreviewRequest, BoardModelPreviews, createBoardAssetManager } from "./assets.js";
|
|
4
4
|
import { toBoardPointerEvent } from "./pointer.js";
|
|
5
5
|
import { createBoardPointerGestures } from "./pointer-gestures.js";
|
|
6
6
|
import { BoardLiveStroke, BoardScene, BoardSceneNode, SceneSyncInput, createBoardScene } from "./scene.js";
|
|
7
7
|
import { BoardItemView, BoardItemViewContext, BoardItemViewInstance, BoardItemViewLayer, createBoardItemViewLayer } from "./views.js";
|
|
8
|
-
import { BoardStage, BoardStageOptions, BoardStagePointerPhase, BoardStageTheme, mountBoardStage } from "./stage.js";
|
|
8
|
+
import { BoardStage, BoardStageOptions, BoardStagePointerPhase, BoardStageStats, BoardStageTheme, mountBoardStage } from "./stage.js";
|
|
9
9
|
import { BoardOverlayInput, drawBoardOverlay, expandFrame } from "./overlay.js";
|
|
10
|
-
export { BoardAssetManager, BoardAssetManagerOptions, BoardImageTexturePool, BoardItemView, BoardItemViewContext, BoardItemViewInstance, BoardItemViewLayer, BoardLiveStroke, BoardModelPreviewRequest, BoardModelPreviews, BoardOverlayInput, BoardScene, BoardSceneNode, BoardStage, BoardStageOptions, BoardStagePointerPhase, BoardStageTheme, COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry,
|
|
10
|
+
export { BoardAssetManager, BoardAssetManagerOptions, BoardImageTexturePool, BoardItemView, BoardItemViewContext, BoardItemViewInstance, BoardItemViewLayer, BoardLiveStroke, BoardModelPreviewRequest, BoardModelPreviews, BoardOverlayInput, BoardScene, BoardSceneNode, BoardStage, BoardStageOptions, BoardStagePointerPhase, BoardStageStats, BoardStageTheme, COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, LruTrim, SceneSyncInput, boardImageTexturePool, createBoardAssetManager, createBoardImageTexturePool, createBoardItemViewLayer, createBoardPointerGestures, createBoardScene, drawBoardOverlay, expandFrame, loadBoardImageTexture, loadFittedImageTexture, mountBoardStage, planLruTrim, selectLruEvictions, shrinkBoardTexture, toBoardPointerEvent, unloadBoardImageTexture };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, selectLruEvictions } from "./lru.js";
|
|
2
|
-
import { boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, unloadBoardImageTexture } from "./texture-pool.js";
|
|
3
|
-
import {
|
|
1
|
+
import { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, planLruTrim, selectLruEvictions } from "./lru.js";
|
|
2
|
+
import { boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, loadFittedImageTexture, shrinkBoardTexture, unloadBoardImageTexture } from "./texture-pool.js";
|
|
3
|
+
import { createBoardAssetManager } from "./assets.js";
|
|
4
4
|
import { toBoardPointerEvent } from "./pointer.js";
|
|
5
5
|
import { createBoardPointerGestures } from "./pointer-gestures.js";
|
|
6
6
|
import { createBoardScene } from "./scene.js";
|
|
7
7
|
import { drawBoardOverlay, expandFrame } from "./overlay.js";
|
|
8
8
|
import { createBoardItemViewLayer } from "./views.js";
|
|
9
9
|
import { mountBoardStage } from "./stage.js";
|
|
10
|
-
export { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET,
|
|
10
|
+
export { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, boardImageTexturePool, createBoardAssetManager, createBoardImageTexturePool, createBoardItemViewLayer, createBoardPointerGestures, createBoardScene, drawBoardOverlay, expandFrame, loadBoardImageTexture, loadFittedImageTexture, mountBoardStage, planLruTrim, selectLruEvictions, shrinkBoardTexture, toBoardPointerEvent, unloadBoardImageTexture };
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* they enter a "cooling" pool (reference count zero but still on the GPU) so a
|
|
7
7
|
* quick pan back is instant instead of re-fetching. The pool is bounded by
|
|
8
8
|
* both entry count and approximate GPU bytes; when a budget is exceeded the least
|
|
9
|
-
* recently used entries
|
|
10
|
-
* release-vs-churn trade-off trivial to test.
|
|
9
|
+
* recently used entries shrink or leave first. Keeping this decision pure makes
|
|
10
|
+
* the release-vs-churn trade-off trivial to test.
|
|
11
11
|
*/
|
|
12
12
|
type LruEntry = {
|
|
13
13
|
key: string;
|
|
@@ -15,19 +15,23 @@ type LruEntry = {
|
|
|
15
15
|
lastUsedAt: number;
|
|
16
16
|
/** Approximate GPU footprint in bytes. */
|
|
17
17
|
bytes: number;
|
|
18
|
+
shrunkBytes?: number;
|
|
18
19
|
};
|
|
19
20
|
type LruBudget = {
|
|
20
21
|
maxCount: number;
|
|
21
22
|
maxBytes: number;
|
|
22
23
|
};
|
|
23
|
-
/** Default cooling-pool ceiling: 512 images or ~192 MB of GPU memory. */
|
|
24
|
-
declare const DEFAULT_LRU_BUDGET: LruBudget;
|
|
25
|
-
declare const COMPACT_LRU_BUDGET: LruBudget;
|
|
26
24
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* an empty array when the pool already fits.
|
|
25
|
+
* Default cooling-pool ceiling: 2048 images or ~192 MB of GPU memory. Bytes
|
|
26
|
+
* bind first for full-size textures; shrunk copies let many more stay warm.
|
|
30
27
|
*/
|
|
28
|
+
declare const DEFAULT_LRU_BUDGET: LruBudget;
|
|
29
|
+
declare const COMPACT_LRU_BUDGET: LruBudget;
|
|
30
|
+
type LruTrim = {
|
|
31
|
+
shrink: string[];
|
|
32
|
+
evict: string[];
|
|
33
|
+
};
|
|
34
|
+
declare function planLruTrim(entries: LruEntry[], budget: LruBudget): LruTrim;
|
|
31
35
|
declare function selectLruEvictions(entries: LruEntry[], budget: LruBudget): string[];
|
|
32
36
|
//#endregion
|
|
33
|
-
export { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, selectLruEvictions };
|
|
37
|
+
export { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, LruTrim, planLruTrim, selectLruEvictions };
|
package/dist/board/stage/lru.js
CHANGED
|
@@ -1,33 +1,51 @@
|
|
|
1
1
|
//#region src/board/stage/lru.ts
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Default cooling-pool ceiling: 2048 images or ~192 MB of GPU memory. Bytes
|
|
4
|
+
* bind first for full-size textures; shrunk copies let many more stay warm.
|
|
5
|
+
*/
|
|
3
6
|
const DEFAULT_LRU_BUDGET = {
|
|
4
|
-
maxCount:
|
|
7
|
+
maxCount: 2048,
|
|
5
8
|
maxBytes: 201326592
|
|
6
9
|
};
|
|
7
10
|
const COMPACT_LRU_BUDGET = {
|
|
8
|
-
maxCount:
|
|
11
|
+
maxCount: 1024,
|
|
9
12
|
maxBytes: 100663296
|
|
10
13
|
};
|
|
11
|
-
|
|
12
|
-
* Given the unreferenced (cooling) entries and a budget, return the keys to
|
|
13
|
-
* evict — oldest first — so the remaining pool fits within both limits. Returns
|
|
14
|
-
* an empty array when the pool already fits.
|
|
15
|
-
*/
|
|
16
|
-
function selectLruEvictions(entries, budget) {
|
|
17
|
-
if (entries.length === 0) return [];
|
|
18
|
-
let count = entries.length;
|
|
14
|
+
function planLruTrim(entries, budget) {
|
|
19
15
|
let bytes = 0;
|
|
20
16
|
for (const entry of entries) bytes += entry.bytes;
|
|
21
|
-
|
|
17
|
+
const over = entries.length - budget.maxCount;
|
|
18
|
+
if (over <= 0 && bytes <= budget.maxBytes) return {
|
|
19
|
+
shrink: [],
|
|
20
|
+
evict: []
|
|
21
|
+
};
|
|
22
22
|
const sorted = [...entries].sort((a, b) => a.lastUsedAt - b.lastUsedAt);
|
|
23
|
-
const evict =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
bytes
|
|
23
|
+
const evict = sorted.slice(0, Math.max(0, over));
|
|
24
|
+
const kept = sorted.slice(evict.length);
|
|
25
|
+
for (const entry of evict) bytes -= entry.bytes;
|
|
26
|
+
const shrink = /* @__PURE__ */ new Set();
|
|
27
|
+
for (const entry of kept) {
|
|
28
|
+
if (bytes <= budget.maxBytes) break;
|
|
29
|
+
if (entry.shrunkBytes === void 0 || entry.shrunkBytes >= entry.bytes) continue;
|
|
30
|
+
shrink.add(entry);
|
|
31
|
+
bytes -= entry.bytes - entry.shrunkBytes;
|
|
32
|
+
}
|
|
33
|
+
for (const entry of kept) {
|
|
34
|
+
if (bytes <= budget.maxBytes) break;
|
|
35
|
+
bytes -= shrink.delete(entry) ? entry.shrunkBytes : entry.bytes;
|
|
36
|
+
evict.push(entry);
|
|
29
37
|
}
|
|
30
|
-
return
|
|
38
|
+
return {
|
|
39
|
+
shrink: [...shrink].map(({ key }) => key),
|
|
40
|
+
evict: evict.map(({ key }) => key)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function selectLruEvictions(entries, budget) {
|
|
44
|
+
return planLruTrim(entries.map(({ key, lastUsedAt, bytes }) => ({
|
|
45
|
+
key,
|
|
46
|
+
lastUsedAt,
|
|
47
|
+
bytes
|
|
48
|
+
})), budget).evict;
|
|
31
49
|
}
|
|
32
50
|
//#endregion
|
|
33
|
-
export { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, selectLruEvictions };
|
|
51
|
+
export { COMPACT_LRU_BUDGET, DEFAULT_LRU_BUDGET, planLruTrim, selectLruEvictions };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { BoardDrawPoint } from "../../protocol/dist/board-model.js";
|
|
2
|
+
import { Rect } from "../model/geometry.js";
|
|
2
3
|
import { BoardScene as BoardScene$1, BoardSceneItem } from "../model/scene.js";
|
|
3
4
|
import "../model/index.js";
|
|
5
|
+
import { BoardLod } from "../render/lod.js";
|
|
4
6
|
import { BoardCardRendererResolver, BoardRenderContext } from "../render/renderers/board-renderer-registry.js";
|
|
5
7
|
import "../render/index.js";
|
|
6
8
|
import { Container, Graphics } from "pixi.js";
|
|
@@ -22,12 +24,14 @@ type SceneSyncInput = {
|
|
|
22
24
|
globalSig: string;
|
|
23
25
|
staleIds?: ReadonlySet<string>;
|
|
24
26
|
structureVersion: number;
|
|
25
|
-
geometryVersion: number;
|
|
26
27
|
gestureActive: boolean;
|
|
27
28
|
focus?: {
|
|
28
29
|
x: number;
|
|
29
30
|
y: number;
|
|
30
31
|
};
|
|
32
|
+
/** The world area on screen; live cards outside it skip rendering. */
|
|
33
|
+
view?: Rect;
|
|
34
|
+
lod?: BoardLod;
|
|
31
35
|
};
|
|
32
36
|
type BoardSceneNode = {
|
|
33
37
|
item: BoardSceneItem;
|
|
@@ -39,8 +43,14 @@ type BoardScene = {
|
|
|
39
43
|
holders: (key: string) => ReadonlySet<string>;
|
|
40
44
|
readonly liveVersion: number;
|
|
41
45
|
liveIds: () => ReadonlySet<string>;
|
|
46
|
+
/** Live cards and cards drawn on far plates. */
|
|
47
|
+
counts: () => {
|
|
48
|
+
live: number;
|
|
49
|
+
far: number;
|
|
50
|
+
};
|
|
42
51
|
readonly animated: boolean;
|
|
43
52
|
tick: (clock: number) => number | null;
|
|
53
|
+
redrawText: () => void;
|
|
44
54
|
destroy: (context: BoardRenderContext) => void;
|
|
45
55
|
};
|
|
46
56
|
declare function createBoardScene(options: {
|