@neta-art/cohub-cli 10.8.2 → 10.10.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 +18 -4
- package/dist/board-export-resources.d.ts +3 -1
- package/dist/board-export-resources.js +13 -2
- package/dist/board-export.d.ts +23 -8
- package/dist/board-export.js +104 -53
- package/dist/board-fonts.d.ts +5 -1
- package/dist/board-fonts.js +8 -8
- package/dist/board-kernel.d.ts +2 -2
- package/dist/board-kernel.js +6088 -470
- package/dist/board-media.d.ts +4 -0
- package/dist/board-media.js +63 -0
- package/dist/commands/boards/authoring.js +3 -1
- package/dist/commands/boards.js +16 -10
- package/dist/commands/feedback.d.ts +2 -0
- package/dist/commands/feedback.js +129 -0
- package/dist/fonts/figtree-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/geist-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/geist-mono-latin-wght-normal.woff2 +0 -0
- package/dist/index.js +3 -0
- package/dist/runtime/native/attach.js +21 -12
- package/dist/runtime/native/transcript.js +19 -4
- package/dist/runtime/supervisor.js +13 -1
- package/dist/space-files/operands.d.ts +1 -0
- package/dist/space-files/operands.js +1 -1
- package/package.json +6 -5
- package/dist/fonts/geist-latin-400-normal.woff2 +0 -0
- package/dist/fonts/geist-latin-500-normal.woff2 +0 -0
- package/dist/fonts/geist-latin-600-normal.woff2 +0 -0
- package/dist/fonts/geist-latin-700-normal.woff2 +0 -0
- package/dist/fonts/geist-mono-latin-500-normal.woff2 +0 -0
- package/dist/fonts/geist-mono-latin-600-normal.woff2 +0 -0
- package/dist/fonts/geist-mono-latin-700-normal.woff2 +0 -0
package/README.md
CHANGED
|
@@ -57,12 +57,15 @@ Sign in once for each custom server, or supply `COHUB_EXECUTION_TOKEN`.
|
|
|
57
57
|
|
|
58
58
|
## Board export resources
|
|
59
59
|
|
|
60
|
-
Board exports keep the requested resolution and
|
|
60
|
+
Board exports keep the requested resolution. Previews are fetched and decoded at the size
|
|
61
|
+
the export needs: Cohub-hosted media (Space files included) provide resized images and
|
|
62
|
+
video first frames, and larger sources are scaled down before decoding. Before rendering,
|
|
61
63
|
`boards export` prints the output dimensions and frame count (except with `--json`).
|
|
62
64
|
Decoded image textures, including background and script output images, share a 256 MiB
|
|
63
|
-
budget.
|
|
64
|
-
|
|
65
|
-
sufficient memory; it is an image budget, not a limit on total
|
|
65
|
+
budget. Previews beyond it are drawn as placeholders with a warning; a background image
|
|
66
|
+
that does not fit fails the export before native decoding. Use `--image-memory <MiB>` to raise the budget
|
|
67
|
+
when the environment has sufficient memory; it is an image budget, not a limit on total
|
|
68
|
+
process memory.
|
|
66
69
|
`--size` controls the viewport only with `--camera`.
|
|
67
70
|
|
|
68
71
|
```bash
|
|
@@ -602,6 +605,17 @@ cohub cron-jobs delete <cronJobId>
|
|
|
602
605
|
|
|
603
606
|
Confirm before enabling, disabling, or deleting recurring scheduled prompts.
|
|
604
607
|
|
|
608
|
+
## Feedback
|
|
609
|
+
|
|
610
|
+
```bash
|
|
611
|
+
cohub feedback "runtime up hangs after sign-in"
|
|
612
|
+
cohub feedback "Board export is blurry" -f board.png -f export.log
|
|
613
|
+
cohub feedback --runtime-logs "Codex chats stopped syncing"
|
|
614
|
+
cat report.md | cohub feedback
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
Sends feedback to the Cohub team. CLI version, OS, environment, account, and `COHUB_*` Space/Chat context are attached; the working directory is not. `--runtime-logs` attaches recent local Runtime diagnostics for the target Space.
|
|
618
|
+
|
|
605
619
|
## Safety
|
|
606
620
|
|
|
607
621
|
Confirm before:
|
|
@@ -2,7 +2,9 @@ import type { BoardNodeRenderer } from "@neta-art/cohub/board/export/node";
|
|
|
2
2
|
export declare const BOARD_EXPORT_IMAGE_MEMORY_MIB = 256;
|
|
3
3
|
export declare class BoardExportResourceError extends Error {
|
|
4
4
|
}
|
|
5
|
+
export type BoardExportDecodeImage = (bytes: Uint8Array, mimeType?: string, maxEdge?: number) => ReturnType<BoardNodeRenderer["decodeImage"]>;
|
|
6
|
+
/** Decodes within `maxBytes`, first scaling images down to `maxEdge` on their long edge. */
|
|
5
7
|
export declare function createBoardExportImageBudget(renderer: BoardNodeRenderer, maxBytes: number): {
|
|
6
|
-
decodeImage:
|
|
8
|
+
decodeImage: BoardExportDecodeImage;
|
|
7
9
|
readonly usedBytes: number;
|
|
8
10
|
};
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
export const BOARD_EXPORT_IMAGE_MEMORY_MIB = 256;
|
|
2
2
|
export class BoardExportResourceError extends Error {
|
|
3
3
|
}
|
|
4
|
+
/** Decodes within `maxBytes`, first scaling images down to `maxEdge` on their long edge. */
|
|
4
5
|
export function createBoardExportImageBudget(renderer, maxBytes) {
|
|
5
6
|
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)
|
|
6
7
|
throw new Error("Image memory budget must be a positive integer.");
|
|
7
8
|
let usedBytes = 0;
|
|
8
|
-
const decodeImage = async (
|
|
9
|
+
const decodeImage = async (input, mimeType, maxEdge = Number.POSITIVE_INFINITY) => {
|
|
9
10
|
const { default: sharp } = await import("sharp");
|
|
10
|
-
|
|
11
|
+
let bytes = input;
|
|
12
|
+
let { width, height } = await sharp(bytes, { limitInputPixels: false }).metadata();
|
|
13
|
+
if (Math.max(width ?? 0, height ?? 0) > maxEdge) {
|
|
14
|
+
const resized = await sharp(bytes, { limitInputPixels: false })
|
|
15
|
+
.resize({ width: maxEdge, height: maxEdge, fit: "inside" })
|
|
16
|
+
.png({ compressionLevel: 0 })
|
|
17
|
+
.toBuffer({ resolveWithObject: true });
|
|
18
|
+
({ width, height } = resized.info);
|
|
19
|
+
bytes = new Uint8Array(resized.data);
|
|
20
|
+
mimeType = "image/png";
|
|
21
|
+
}
|
|
11
22
|
const required = (width ?? 0) * (height ?? 0) * 4;
|
|
12
23
|
if (!Number.isSafeInteger(required) || required <= 0)
|
|
13
24
|
throw new Error("Image dimensions could not be read.");
|
package/dist/board-export.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { BoardDocument, BoardExportRegion, BoardSceneItem } from "@neta-art/cohub/board";
|
|
2
|
-
import type { BoardNodeExportFormat, BoardNodeFont,
|
|
1
|
+
import type { BoardDocument, BoardExportRegion, BoardSceneItem, BoardThemeId } from "@neta-art/cohub/board";
|
|
2
|
+
import type { BoardNodeExportFormat, BoardNodeFont, BoardNodeTexture } from "@neta-art/cohub/board/export/node";
|
|
3
|
+
import { type BoardExportDecodeImage } from "./board-export-resources.js";
|
|
3
4
|
export declare function resolveBundledFonts(): BoardNodeFont[];
|
|
4
5
|
export type BoardExportSource = {
|
|
5
6
|
document: BoardDocument;
|
|
@@ -7,18 +8,31 @@ export type BoardExportSource = {
|
|
|
7
8
|
title: string | null;
|
|
8
9
|
};
|
|
9
10
|
export declare function loadBoardDocument(spaceId: string, target: string): Promise<BoardExportSource>;
|
|
11
|
+
type TextureMiss = "error" | "unavailable" | "omitted";
|
|
10
12
|
export type BoardExportTextureCache = {
|
|
11
13
|
textures: Map<string, BoardNodeTexture>;
|
|
12
|
-
|
|
14
|
+
/** Long edge each texture was decoded for; Infinity once it is the whole source. */
|
|
15
|
+
edges: Map<string, number>;
|
|
16
|
+
/** Keys without a texture: failed to load, no picture Node can draw, or over the image budget. */
|
|
17
|
+
missed: Map<string, TextureMiss>;
|
|
13
18
|
};
|
|
14
|
-
|
|
19
|
+
/** Texture keys Node can draw; 3D model stills need a browser. */
|
|
20
|
+
export declare function boardExportAssetKey(item: BoardSceneItem): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* Previews for `items` within what the image budget leaves after other images,
|
|
23
|
+
* such as the background. A preview that still does not fit is omitted.
|
|
24
|
+
*/
|
|
25
|
+
export declare function loadBoardTextures(images: {
|
|
26
|
+
decodeImage: BoardExportDecodeImage;
|
|
27
|
+
usedBytes: number;
|
|
28
|
+
}, spaceId: string, items: readonly BoardSceneItem[], options: {
|
|
29
|
+
scale: number;
|
|
30
|
+
maxBytes: number;
|
|
15
31
|
concurrency?: number;
|
|
16
32
|
cache?: BoardExportTextureCache;
|
|
17
33
|
}): Promise<{
|
|
18
34
|
textures: Map<string, BoardNodeTexture>;
|
|
19
|
-
|
|
20
|
-
omitted: string[];
|
|
21
|
-
}>;
|
|
35
|
+
} & Record<"failed" | "unavailable" | "omitted", string[]>>;
|
|
22
36
|
export type BoardExportRunOptions = {
|
|
23
37
|
spaceId: string;
|
|
24
38
|
target: string;
|
|
@@ -27,7 +41,7 @@ export type BoardExportRunOptions = {
|
|
|
27
41
|
animation?: string;
|
|
28
42
|
scale: number;
|
|
29
43
|
padding?: number;
|
|
30
|
-
|
|
44
|
+
theme: BoardThemeId;
|
|
31
45
|
background: "paper" | "transparent";
|
|
32
46
|
format: BoardNodeExportFormat;
|
|
33
47
|
quality?: number;
|
|
@@ -74,3 +88,4 @@ export declare function runBoardVideoExport(options: BoardVideoExportOptions): P
|
|
|
74
88
|
warnings: string[];
|
|
75
89
|
}>;
|
|
76
90
|
export declare function runBoardExport(options: BoardExportRunOptions): Promise<BoardExportRunResult | null>;
|
|
91
|
+
export {};
|
package/dist/board-export.js
CHANGED
|
@@ -10,12 +10,14 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
const execFile = promisify(execFileCallback);
|
|
12
12
|
import { parseBoardMediaSource } from "@neta-art/cohub/board";
|
|
13
|
-
import {
|
|
13
|
+
import { imageVariantUrl, MEDIA_VARIANT_MAX, mediaVariantSize, videoFrameUrl } from "@neta-art/cohub/media";
|
|
14
|
+
import { hasBoardScripts, boardAssetKey, boardAssetKeySource, boardDocumentAt, boardRenderTheme, boardTextureEdge, boardTexturePixels, createBoardRenderSampler, buildBoardScene, createNodeBoardRenderer, createNodeBoardScriptHost, exportBoardImageBytes, parseBoardDocument, planBoardExport, prepareBoardExport, selectBoardExportAssets, } from "./board-kernel.js";
|
|
14
15
|
import { resolveBoardId } from "./board-command-support.js";
|
|
16
|
+
import { settleBoardDocument } from "./board-media.js";
|
|
15
17
|
import { BOARD_EXPORT_FONTS } from "./board-fonts.js";
|
|
16
18
|
import { createClient } from "./client.js";
|
|
17
19
|
import { downloadPublicAudio, downloadPublicImage } from "./safe-remote-image.js";
|
|
18
|
-
import { BOARD_EXPORT_IMAGE_MEMORY_MIB, BoardExportResourceError, createBoardExportImageBudget } from "./board-export-resources.js";
|
|
20
|
+
import { BOARD_EXPORT_IMAGE_MEMORY_MIB, BoardExportResourceError, createBoardExportImageBudget, } from "./board-export-resources.js";
|
|
19
21
|
const debugExport = debuglog("cohub:board-export");
|
|
20
22
|
export function resolveBundledFonts() {
|
|
21
23
|
const require = createRequire(import.meta.url);
|
|
@@ -53,55 +55,105 @@ export async function loadBoardDocument(spaceId, target) {
|
|
|
53
55
|
throw new Error(`Board ${result.id} could not be read: ${parsed.diagnostics[0]?.path}: ${parsed.diagnostics[0]?.message}`);
|
|
54
56
|
return { document: parsed.document, boardId: result.id, title: result.title };
|
|
55
57
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
58
|
+
/** Texture keys Node can draw; 3D model stills need a browser. */
|
|
59
|
+
export function boardExportAssetKey(item) {
|
|
60
|
+
const key = boardAssetKey(item);
|
|
61
|
+
return key && boardAssetKeySource(key)?.media !== "model" ? key : null;
|
|
62
|
+
}
|
|
63
|
+
function textureEdge(item, scale) {
|
|
64
|
+
const edge = boardTextureEdge(item, scale);
|
|
65
|
+
return Number.isFinite(edge) ? edge : Math.ceil(boardTexturePixels(item, scale));
|
|
66
|
+
}
|
|
67
|
+
const textureBytes = (texture) => texture.width * texture.height * 4;
|
|
68
|
+
/**
|
|
69
|
+
* Previews for `items` within what the image budget leaves after other images,
|
|
70
|
+
* such as the background. A preview that still does not fit is omitted.
|
|
71
|
+
*/
|
|
72
|
+
export async function loadBoardTextures(images, spaceId, items, options) {
|
|
73
|
+
const cache = options.cache ?? { textures: new Map(), edges: new Map(), missed: new Map() };
|
|
74
|
+
const { textures, edges, missed } = cache;
|
|
75
|
+
let previewBytes = 0;
|
|
76
|
+
for (const texture of textures.values())
|
|
77
|
+
previewBytes += textureBytes(texture);
|
|
78
|
+
const maxBytes = options.maxBytes - (images.usedBytes - previewBytes);
|
|
79
|
+
const selection = selectBoardExportAssets([...items], boardExportAssetKey, { scale: options.scale, maxBytes });
|
|
60
80
|
const wanted = new Set(selection.keys);
|
|
61
81
|
for (const [key, texture] of textures) {
|
|
62
82
|
if (wanted.has(key))
|
|
63
83
|
continue;
|
|
64
84
|
texture.destroy(true);
|
|
65
85
|
textures.delete(key);
|
|
86
|
+
edges.delete(key);
|
|
66
87
|
}
|
|
67
|
-
for (const key of
|
|
88
|
+
for (const key of missed.keys())
|
|
68
89
|
if (!wanted.has(key))
|
|
69
|
-
|
|
70
|
-
const pending = selection.
|
|
90
|
+
missed.delete(key);
|
|
91
|
+
const pending = selection.items.flatMap((item, index) => {
|
|
92
|
+
const key = selection.keys[index];
|
|
93
|
+
const edge = textureEdge(item, options.scale);
|
|
94
|
+
return missed.has(key) || (edges.get(key) ?? 0) >= edge ? [] : [{ key, item, edge }];
|
|
95
|
+
});
|
|
71
96
|
const concurrency = Math.max(1, Math.min(options.concurrency ?? 2, 16));
|
|
72
97
|
const client = createClient();
|
|
73
|
-
let resourceError;
|
|
74
98
|
async function worker() {
|
|
75
|
-
for (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const key = pending.shift();
|
|
79
|
-
if (!key)
|
|
80
|
-
return;
|
|
81
|
-
const source = boardImageKeySource(key);
|
|
82
|
-
if (!source) {
|
|
83
|
-
failed.add(key);
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
99
|
+
for (let task = pending.shift(); task; task = pending.shift()) {
|
|
100
|
+
const { key, item, edge } = task;
|
|
101
|
+
let miss = "unavailable";
|
|
86
102
|
try {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
103
|
+
const source = boardAssetKeySource(key);
|
|
104
|
+
const picture = source && await fetchBoardPicture(client, spaceId, source, item, edge);
|
|
105
|
+
if (picture) {
|
|
106
|
+
const texture = await images.decodeImage(picture.bytes, picture.mimeType, edge);
|
|
107
|
+
textures.get(key)?.destroy(true);
|
|
108
|
+
textures.set(key, texture);
|
|
109
|
+
edges.set(key, Math.max(texture.width, texture.height) < edge ? Number.POSITIVE_INFINITY : edge);
|
|
110
|
+
miss = null;
|
|
111
|
+
}
|
|
92
112
|
}
|
|
93
113
|
catch (error) {
|
|
94
|
-
|
|
95
|
-
resourceError = error;
|
|
96
|
-
else
|
|
97
|
-
failed.add(key);
|
|
114
|
+
miss = error instanceof BoardExportResourceError ? "omitted" : "error";
|
|
98
115
|
}
|
|
116
|
+
// A preview already shown stays, at the sharpness it has.
|
|
117
|
+
if (miss && textures.has(key))
|
|
118
|
+
edges.set(key, Number.POSITIVE_INFINITY);
|
|
119
|
+
else if (miss)
|
|
120
|
+
missed.set(key, miss);
|
|
99
121
|
}
|
|
100
122
|
}
|
|
101
123
|
await Promise.all(Array.from({ length: Math.min(concurrency, pending.length) }, worker));
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
124
|
+
const keys = (reason) => [...missed].flatMap(([key, value]) => (value === reason ? [key] : []));
|
|
125
|
+
return { textures, failed: keys("error"), unavailable: keys("unavailable"), omitted: [...selection.omittedKeys, ...keys("omitted")] };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The picture for a texture, `edge` pixels on its long edge where the host can
|
|
129
|
+
* resize: a CDN variant of an image, or the first frame of a video. Null when
|
|
130
|
+
* there is none, as for videos outside Cohub's media hosts.
|
|
131
|
+
*/
|
|
132
|
+
async function fetchBoardPicture(client, spaceId, source, item, edge) {
|
|
133
|
+
// Thumbnail edges below the smallest variant fetch that variant; decoding scales it down.
|
|
134
|
+
const variantEdge = Number.isFinite(edge) && edge <= MEDIA_VARIANT_MAX ? mediaVariantSize(edge) : null;
|
|
135
|
+
const video = source.media === "video";
|
|
136
|
+
const url = source.kind === "url"
|
|
137
|
+
? source.value
|
|
138
|
+
: video || variantEdge
|
|
139
|
+
? await client.space(spaceId).files.resolveUrl(source.value, { purpose: "playback" }).catch(() => null)
|
|
140
|
+
: null;
|
|
141
|
+
if (video) {
|
|
142
|
+
const { naturalWidth: w, naturalHeight: h } = (item.type === "video" && item.props.snapshot) || {};
|
|
143
|
+
const width = w && h ? Math.min(w, (edge * w) / Math.max(w, h)) : edge;
|
|
144
|
+
const frame = url && videoFrameUrl(url, mediaVariantSize(width));
|
|
145
|
+
return frame ? downloadPublicImage(frame) : null;
|
|
146
|
+
}
|
|
147
|
+
const variant = url && variantEdge && imageVariantUrl(url, variantEdge);
|
|
148
|
+
if (variant) {
|
|
149
|
+
try {
|
|
150
|
+
return await downloadPublicImage(variant);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Fall back to the original below.
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return source.kind === "url" ? downloadPublicImage(source.value) : readSpaceFileBytes(client, spaceId, source.value);
|
|
105
157
|
}
|
|
106
158
|
async function readSpaceFileBytes(client, spaceId, path) {
|
|
107
159
|
const { blob, mimeType } = await client.space(spaceId).files.download(path);
|
|
@@ -122,7 +174,7 @@ export async function runBoardVideoExport(options) {
|
|
|
122
174
|
if (times.some((time, index) => index > 0 && Math.abs(time - times[index - 1] - step) > 0.001))
|
|
123
175
|
throw new Error("Video export needs evenly spaced times.");
|
|
124
176
|
await execFile("ffmpeg", ["-version"], { signal: options.signal }).catch(() => { throw new Error("Video export needs ffmpeg. Install ffmpeg and try again."); });
|
|
125
|
-
const document = options.document ?? (await loadBoardDocument(options.spaceId, options.target)).document;
|
|
177
|
+
const document = options.document ?? await settleBoardDocument(options.spaceId, (await loadBoardDocument(options.spaceId, options.target)).document);
|
|
126
178
|
const tempDir = await mkdtemp(join(tmpdir(), "cohub-board-export-"));
|
|
127
179
|
try {
|
|
128
180
|
let count = 0;
|
|
@@ -208,7 +260,7 @@ export async function runBoardVideoExport(options) {
|
|
|
208
260
|
}
|
|
209
261
|
export async function runBoardExport(options) {
|
|
210
262
|
options.signal?.throwIfAborted();
|
|
211
|
-
const document = options.document ?? (await loadBoardDocument(options.spaceId, options.target)).document;
|
|
263
|
+
const document = options.document ?? await settleBoardDocument(options.spaceId, (await loadBoardDocument(options.spaceId, options.target)).document);
|
|
212
264
|
if (options.animation && !document.animations[options.animation]) {
|
|
213
265
|
throw new Error(`Animation ${options.animation} does not exist.`);
|
|
214
266
|
}
|
|
@@ -230,10 +282,11 @@ export async function runBoardExport(options) {
|
|
|
230
282
|
});
|
|
231
283
|
};
|
|
232
284
|
let scriptHost = null;
|
|
233
|
-
const textureCache = { textures: new Map(),
|
|
285
|
+
const textureCache = { textures: new Map(), edges: new Map(), missed: new Map() };
|
|
234
286
|
let backgroundTexture;
|
|
235
287
|
try {
|
|
236
|
-
const
|
|
288
|
+
const theme = boardRenderTheme(options.theme);
|
|
289
|
+
const sampler = createBoardRenderSampler(document, options.animation ?? null, theme);
|
|
237
290
|
const documentAt = (time) => time === null ? document : sampler.at(time).document;
|
|
238
291
|
const plan = planBoardExport({
|
|
239
292
|
scene: buildBoardScene(documentAt(times[0] ?? null)),
|
|
@@ -272,7 +325,6 @@ export async function runBoardExport(options) {
|
|
|
272
325
|
warnings.push("The board background image could not be loaded; the fallback color was exported.");
|
|
273
326
|
}
|
|
274
327
|
}
|
|
275
|
-
const videoCount = plan.items.filter((item) => item.type === "video").length;
|
|
276
328
|
if (hasBoardScripts(document))
|
|
277
329
|
scriptHost = createNodeBoardScriptHost(renderer, {
|
|
278
330
|
concurrency: 2,
|
|
@@ -282,18 +334,13 @@ export async function runBoardExport(options) {
|
|
|
282
334
|
},
|
|
283
335
|
});
|
|
284
336
|
const host = scriptHost;
|
|
285
|
-
if (videoCount > 0) {
|
|
286
|
-
warnings.push(`${videoCount} video preview${videoCount === 1 ? " was" : "s were"} drawn as placeholders; video decoding is unavailable in Node.`);
|
|
287
|
-
}
|
|
288
337
|
const backgroundImage = backgroundTexture
|
|
289
338
|
? { texture: backgroundTexture, fit: declaredBackground.fit ?? "cover", position: "center", opacity: declaredBackground.opacity ?? 1 }
|
|
290
339
|
: undefined;
|
|
291
|
-
const assetKey =
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
: undefined;
|
|
340
|
+
const assetKey = (item) => {
|
|
341
|
+
const key = boardExportAssetKey(item);
|
|
342
|
+
return key && omittedKeys.has(key) ? null : key;
|
|
343
|
+
};
|
|
297
344
|
const frames = [];
|
|
298
345
|
const reported = new Set();
|
|
299
346
|
let completed = 0;
|
|
@@ -320,11 +367,15 @@ export async function runBoardExport(options) {
|
|
|
320
367
|
}
|
|
321
368
|
trace("images", completed, prepared.plan);
|
|
322
369
|
if (options.withImages) {
|
|
323
|
-
const
|
|
370
|
+
const visible = prepared.plan.items.filter((item) => prepared.scene.opacity(item.id) > 0);
|
|
371
|
+
const loaded = await loadBoardTextures(images, options.spaceId, visible, { scale: prepared.plan.scale, maxBytes: imageMemoryBytes, cache: textureCache });
|
|
324
372
|
omittedKeys = new Set(loaded.omitted);
|
|
373
|
+
const sources = (keys) => keys.slice(0, 3).map((key) => boardAssetKeySource(key)?.value ?? key).join(", ");
|
|
374
|
+
const plural = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
325
375
|
const assetWarnings = [
|
|
326
|
-
loaded.failed.length ? `${loaded.failed.length
|
|
327
|
-
loaded.
|
|
376
|
+
loaded.failed.length ? `${plural(loaded.failed.length, "preview")} could not be loaded: ${sources(loaded.failed)}` : null,
|
|
377
|
+
loaded.unavailable.length ? `${plural(loaded.unavailable.length, "video")} drawn without a cover; Node exports take covers from Cohub-hosted media only.` : null,
|
|
378
|
+
loaded.omitted.length ? `${plural(loaded.omitted.length, "preview")} drawn as placeholders to stay within the ${imageMemoryBytes / 1024 ** 2} MiB image budget; raise --image-memory to include them.` : null,
|
|
328
379
|
];
|
|
329
380
|
for (const warning of assetWarnings)
|
|
330
381
|
if (warning && !reported.has(warning)) {
|
|
@@ -335,12 +386,12 @@ export async function runBoardExport(options) {
|
|
|
335
386
|
trace("render", completed, prepared.plan);
|
|
336
387
|
const result = exportBoardImageBytes(renderer, document, {
|
|
337
388
|
...frameOptions,
|
|
338
|
-
|
|
389
|
+
...theme,
|
|
339
390
|
background: options.background,
|
|
340
391
|
textures,
|
|
341
392
|
scripts: scriptHost ?? undefined,
|
|
342
393
|
backgroundImage,
|
|
343
|
-
|
|
394
|
+
assetKey,
|
|
344
395
|
format: options.format,
|
|
345
396
|
quality: options.quality,
|
|
346
397
|
});
|
|
@@ -387,7 +438,7 @@ function describeWarning(warning) {
|
|
|
387
438
|
}
|
|
388
439
|
if (warning.kind === "images-missing") {
|
|
389
440
|
const keys = warning.keys;
|
|
390
|
-
return `${keys.length}
|
|
441
|
+
return `${keys.length} preview${keys.length === 1 ? "" : "s"} drawn as placeholders; omit --no-images to fetch them.`;
|
|
391
442
|
}
|
|
392
443
|
if (warning.kind === "many-items") {
|
|
393
444
|
return `${warning.count} items exported.`;
|
package/dist/board-fonts.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Fonts `boards export` registers, copied into `dist/fonts` at build time. The
|
|
3
|
+
* variable files and family names are the ones the Web loads, so every weight
|
|
4
|
+
* draws with the same outlines; Figtree is the Neta Pixel theme's font.
|
|
5
|
+
*/
|
|
2
6
|
export type BoardExportFont = {
|
|
3
7
|
pkg: string;
|
|
4
8
|
file: string;
|
package/dist/board-fonts.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}));
|
|
1
|
+
/**
|
|
2
|
+
* Fonts `boards export` registers, copied into `dist/fonts` at build time. The
|
|
3
|
+
* variable files and family names are the ones the Web loads, so every weight
|
|
4
|
+
* draws with the same outlines; Figtree is the Neta Pixel theme's font.
|
|
5
|
+
*/
|
|
7
6
|
export const BOARD_EXPORT_FONTS = [
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
{ pkg: "@fontsource-variable/geist", file: "geist-latin-wght-normal.woff2", family: "Geist Variable" },
|
|
8
|
+
{ pkg: "@fontsource-variable/geist-mono", file: "geist-mono-latin-wght-normal.woff2", family: "Geist Mono Variable" },
|
|
9
|
+
{ pkg: "@fontsource-variable/figtree", file: "figtree-latin-wght-normal.woff2", family: "Figtree Variable" },
|
|
10
10
|
];
|
package/dist/board-kernel.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { BOARD_SCRIPT_DRAWING_EXAMPLE, hasBoardScripts, boardDocumentAt, createBoardAnimationSampler, boardSequence, boardSequencePatch, boardPathTrack,
|
|
1
|
+
export { BOARD_SCRIPT_DRAWING_EXAMPLE, hasBoardScripts, boardAssetKey, boardAssetKeySource, boardDocumentAt, createBoardAnimationSampler, boardSequence, boardSequencePatch, boardPathTrack, boardTextureEdge, boardTexturePixels, buildBoardScene, parseBoardDocument, planBoardExport, selectBoardExportAssets, } from "@neta-art/cohub/board";
|
|
2
2
|
export { prepareBoardExport } from "@neta-art/cohub/board/export";
|
|
3
|
-
export { createBoardRenderSampler, inspectBoardScene, inspectBoardItems, measureBoardText } from "@neta-art/cohub/board/render";
|
|
3
|
+
export { boardRenderTheme, createBoardRenderSampler, inspectBoardScene, inspectBoardItems, measureBoardText } from "@neta-art/cohub/board/render";
|
|
4
4
|
export { createNodeBoardRenderer, createNodeBoardScriptHost, exportBoardImageBytes } from "@neta-art/cohub/board/export/node";
|