@neta-art/cohub-cli 10.8.3 → 10.11.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-command-support.d.ts +0 -1
- package/dist/board-command-support.js +0 -11
- 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 +105 -55
- 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 +6293 -520
- 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 +20 -14
- 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/supervisor.js +37 -2
- 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:
|
|
@@ -3,5 +3,4 @@ export declare const BOARD_CREATE_INPUT_MAX_BYTES: number;
|
|
|
3
3
|
export declare const BOARD_DOMAIN_INPUT_MAX_BYTES: number;
|
|
4
4
|
export declare function parseBoardJsonObject(text: string, source?: string): Record<string, unknown>;
|
|
5
5
|
export declare function readBoardJsonObject(source: string, maxBytes?: number): Promise<Record<string, unknown>>;
|
|
6
|
-
export declare function resolveBoardId(spaceId: string, target: string): Promise<string>;
|
|
7
6
|
export declare function writeBoardOutput(path: string, bytes: Uint8Array, force?: boolean): Promise<void>;
|
|
@@ -2,8 +2,6 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { access, link, open, rename, unlink } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
|
-
import { isBoardPath, parseBoardManifest, } from "@neta-art/cohub/board";
|
|
6
|
-
import { createClient } from "./client.js";
|
|
7
5
|
export const BOARD_TRANSACTION_INPUT_MAX_BYTES = 16 * 1024 * 1024;
|
|
8
6
|
export const BOARD_CREATE_INPUT_MAX_BYTES = 32 * 1024 * 1024;
|
|
9
7
|
export const BOARD_DOMAIN_INPUT_MAX_BYTES = 1024 * 1024;
|
|
@@ -62,15 +60,6 @@ export async function readBoardJsonObject(source, maxBytes = BOARD_DOMAIN_INPUT_
|
|
|
62
60
|
await handle.close();
|
|
63
61
|
}
|
|
64
62
|
}
|
|
65
|
-
export async function resolveBoardId(spaceId, target) {
|
|
66
|
-
if (!isBoardPath(target))
|
|
67
|
-
return target;
|
|
68
|
-
const file = await createClient().space(spaceId).files.read(target);
|
|
69
|
-
if (!("content" in file) || typeof file.content !== "string") {
|
|
70
|
-
throw new Error(`${target} is not a readable Board manifest`);
|
|
71
|
-
}
|
|
72
|
-
return parseBoardManifest(file.content).boardId;
|
|
73
|
-
}
|
|
74
63
|
export async function writeBoardOutput(path, bytes, force = false) {
|
|
75
64
|
if (!force) {
|
|
76
65
|
await access(path).then(() => { throw new Error(`Output already exists: ${path}; use --force to replace it`); }, () => undefined);
|
|
@@ -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,13 @@ 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 {
|
|
14
|
-
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";
|
|
15
|
+
import { settleBoardDocument } from "./board-media.js";
|
|
15
16
|
import { BOARD_EXPORT_FONTS } from "./board-fonts.js";
|
|
16
17
|
import { createClient } from "./client.js";
|
|
17
18
|
import { downloadPublicAudio, downloadPublicImage } from "./safe-remote-image.js";
|
|
18
|
-
import { BOARD_EXPORT_IMAGE_MEMORY_MIB, BoardExportResourceError, createBoardExportImageBudget } from "./board-export-resources.js";
|
|
19
|
+
import { BOARD_EXPORT_IMAGE_MEMORY_MIB, BoardExportResourceError, createBoardExportImageBudget, } from "./board-export-resources.js";
|
|
19
20
|
const debugExport = debuglog("cohub:board-export");
|
|
20
21
|
export function resolveBundledFonts() {
|
|
21
22
|
const require = createRequire(import.meta.url);
|
|
@@ -38,7 +39,7 @@ export function resolveBundledFonts() {
|
|
|
38
39
|
});
|
|
39
40
|
}
|
|
40
41
|
export async function loadBoardDocument(spaceId, target) {
|
|
41
|
-
const board = createClient().space(spaceId).
|
|
42
|
+
const board = await createClient().space(spaceId).boards.open(target);
|
|
42
43
|
let result = await board.get();
|
|
43
44
|
const snapshot = result;
|
|
44
45
|
const items = { ...result.items };
|
|
@@ -53,55 +54,105 @@ export async function loadBoardDocument(spaceId, target) {
|
|
|
53
54
|
throw new Error(`Board ${result.id} could not be read: ${parsed.diagnostics[0]?.path}: ${parsed.diagnostics[0]?.message}`);
|
|
54
55
|
return { document: parsed.document, boardId: result.id, title: result.title };
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
57
|
+
/** Texture keys Node can draw; 3D model stills need a browser. */
|
|
58
|
+
export function boardExportAssetKey(item) {
|
|
59
|
+
const key = boardAssetKey(item);
|
|
60
|
+
return key && boardAssetKeySource(key)?.media !== "model" ? key : null;
|
|
61
|
+
}
|
|
62
|
+
function textureEdge(item, scale) {
|
|
63
|
+
const edge = boardTextureEdge(item, scale);
|
|
64
|
+
return Number.isFinite(edge) ? edge : Math.ceil(boardTexturePixels(item, scale));
|
|
65
|
+
}
|
|
66
|
+
const textureBytes = (texture) => texture.width * texture.height * 4;
|
|
67
|
+
/**
|
|
68
|
+
* Previews for `items` within what the image budget leaves after other images,
|
|
69
|
+
* such as the background. A preview that still does not fit is omitted.
|
|
70
|
+
*/
|
|
71
|
+
export async function loadBoardTextures(images, spaceId, items, options) {
|
|
72
|
+
const cache = options.cache ?? { textures: new Map(), edges: new Map(), missed: new Map() };
|
|
73
|
+
const { textures, edges, missed } = cache;
|
|
74
|
+
let previewBytes = 0;
|
|
75
|
+
for (const texture of textures.values())
|
|
76
|
+
previewBytes += textureBytes(texture);
|
|
77
|
+
const maxBytes = options.maxBytes - (images.usedBytes - previewBytes);
|
|
78
|
+
const selection = selectBoardExportAssets([...items], boardExportAssetKey, { scale: options.scale, maxBytes });
|
|
60
79
|
const wanted = new Set(selection.keys);
|
|
61
80
|
for (const [key, texture] of textures) {
|
|
62
81
|
if (wanted.has(key))
|
|
63
82
|
continue;
|
|
64
83
|
texture.destroy(true);
|
|
65
84
|
textures.delete(key);
|
|
85
|
+
edges.delete(key);
|
|
66
86
|
}
|
|
67
|
-
for (const key of
|
|
87
|
+
for (const key of missed.keys())
|
|
68
88
|
if (!wanted.has(key))
|
|
69
|
-
|
|
70
|
-
const pending = selection.
|
|
89
|
+
missed.delete(key);
|
|
90
|
+
const pending = selection.items.flatMap((item, index) => {
|
|
91
|
+
const key = selection.keys[index];
|
|
92
|
+
const edge = textureEdge(item, options.scale);
|
|
93
|
+
return missed.has(key) || (edges.get(key) ?? 0) >= edge ? [] : [{ key, item, edge }];
|
|
94
|
+
});
|
|
71
95
|
const concurrency = Math.max(1, Math.min(options.concurrency ?? 2, 16));
|
|
72
96
|
const client = createClient();
|
|
73
|
-
let resourceError;
|
|
74
97
|
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
|
-
}
|
|
98
|
+
for (let task = pending.shift(); task; task = pending.shift()) {
|
|
99
|
+
const { key, item, edge } = task;
|
|
100
|
+
let miss = "unavailable";
|
|
86
101
|
try {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
const source = boardAssetKeySource(key);
|
|
103
|
+
const picture = source && await fetchBoardPicture(client, spaceId, source, item, edge);
|
|
104
|
+
if (picture) {
|
|
105
|
+
const texture = await images.decodeImage(picture.bytes, picture.mimeType, edge);
|
|
106
|
+
textures.get(key)?.destroy(true);
|
|
107
|
+
textures.set(key, texture);
|
|
108
|
+
edges.set(key, Math.max(texture.width, texture.height) < edge ? Number.POSITIVE_INFINITY : edge);
|
|
109
|
+
miss = null;
|
|
110
|
+
}
|
|
92
111
|
}
|
|
93
112
|
catch (error) {
|
|
94
|
-
|
|
95
|
-
resourceError = error;
|
|
96
|
-
else
|
|
97
|
-
failed.add(key);
|
|
113
|
+
miss = error instanceof BoardExportResourceError ? "omitted" : "error";
|
|
98
114
|
}
|
|
115
|
+
// A preview already shown stays, at the sharpness it has.
|
|
116
|
+
if (miss && textures.has(key))
|
|
117
|
+
edges.set(key, Number.POSITIVE_INFINITY);
|
|
118
|
+
else if (miss)
|
|
119
|
+
missed.set(key, miss);
|
|
99
120
|
}
|
|
100
121
|
}
|
|
101
122
|
await Promise.all(Array.from({ length: Math.min(concurrency, pending.length) }, worker));
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
123
|
+
const keys = (reason) => [...missed].flatMap(([key, value]) => (value === reason ? [key] : []));
|
|
124
|
+
return { textures, failed: keys("error"), unavailable: keys("unavailable"), omitted: [...selection.omittedKeys, ...keys("omitted")] };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The picture for a texture, `edge` pixels on its long edge where the host can
|
|
128
|
+
* resize: a CDN variant of an image, or the first frame of a video. Null when
|
|
129
|
+
* there is none, as for videos outside Cohub's media hosts.
|
|
130
|
+
*/
|
|
131
|
+
async function fetchBoardPicture(client, spaceId, source, item, edge) {
|
|
132
|
+
// Thumbnail edges below the smallest variant fetch that variant; decoding scales it down.
|
|
133
|
+
const variantEdge = Number.isFinite(edge) && edge <= MEDIA_VARIANT_MAX ? mediaVariantSize(edge) : null;
|
|
134
|
+
const video = source.media === "video";
|
|
135
|
+
const url = source.kind === "url"
|
|
136
|
+
? source.value
|
|
137
|
+
: video || variantEdge
|
|
138
|
+
? await client.space(spaceId).files.resolveUrl(source.value, { purpose: "playback" }).catch(() => null)
|
|
139
|
+
: null;
|
|
140
|
+
if (video) {
|
|
141
|
+
const { naturalWidth: w, naturalHeight: h } = (item.type === "video" && item.props.snapshot) || {};
|
|
142
|
+
const width = w && h ? Math.min(w, (edge * w) / Math.max(w, h)) : edge;
|
|
143
|
+
const frame = url && videoFrameUrl(url, mediaVariantSize(width));
|
|
144
|
+
return frame ? downloadPublicImage(frame) : null;
|
|
145
|
+
}
|
|
146
|
+
const variant = url && variantEdge && imageVariantUrl(url, variantEdge);
|
|
147
|
+
if (variant) {
|
|
148
|
+
try {
|
|
149
|
+
return await downloadPublicImage(variant);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Fall back to the original below.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return source.kind === "url" ? downloadPublicImage(source.value) : readSpaceFileBytes(client, spaceId, source.value);
|
|
105
156
|
}
|
|
106
157
|
async function readSpaceFileBytes(client, spaceId, path) {
|
|
107
158
|
const { blob, mimeType } = await client.space(spaceId).files.download(path);
|
|
@@ -122,7 +173,7 @@ export async function runBoardVideoExport(options) {
|
|
|
122
173
|
if (times.some((time, index) => index > 0 && Math.abs(time - times[index - 1] - step) > 0.001))
|
|
123
174
|
throw new Error("Video export needs evenly spaced times.");
|
|
124
175
|
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;
|
|
176
|
+
const document = options.document ?? await settleBoardDocument(options.spaceId, (await loadBoardDocument(options.spaceId, options.target)).document);
|
|
126
177
|
const tempDir = await mkdtemp(join(tmpdir(), "cohub-board-export-"));
|
|
127
178
|
try {
|
|
128
179
|
let count = 0;
|
|
@@ -208,7 +259,7 @@ export async function runBoardVideoExport(options) {
|
|
|
208
259
|
}
|
|
209
260
|
export async function runBoardExport(options) {
|
|
210
261
|
options.signal?.throwIfAborted();
|
|
211
|
-
const document = options.document ?? (await loadBoardDocument(options.spaceId, options.target)).document;
|
|
262
|
+
const document = options.document ?? await settleBoardDocument(options.spaceId, (await loadBoardDocument(options.spaceId, options.target)).document);
|
|
212
263
|
if (options.animation && !document.animations[options.animation]) {
|
|
213
264
|
throw new Error(`Animation ${options.animation} does not exist.`);
|
|
214
265
|
}
|
|
@@ -230,10 +281,11 @@ export async function runBoardExport(options) {
|
|
|
230
281
|
});
|
|
231
282
|
};
|
|
232
283
|
let scriptHost = null;
|
|
233
|
-
const textureCache = { textures: new Map(),
|
|
284
|
+
const textureCache = { textures: new Map(), edges: new Map(), missed: new Map() };
|
|
234
285
|
let backgroundTexture;
|
|
235
286
|
try {
|
|
236
|
-
const
|
|
287
|
+
const theme = boardRenderTheme(options.theme);
|
|
288
|
+
const sampler = createBoardRenderSampler(document, options.animation ?? null, theme);
|
|
237
289
|
const documentAt = (time) => time === null ? document : sampler.at(time).document;
|
|
238
290
|
const plan = planBoardExport({
|
|
239
291
|
scene: buildBoardScene(documentAt(times[0] ?? null)),
|
|
@@ -272,7 +324,6 @@ export async function runBoardExport(options) {
|
|
|
272
324
|
warnings.push("The board background image could not be loaded; the fallback color was exported.");
|
|
273
325
|
}
|
|
274
326
|
}
|
|
275
|
-
const videoCount = plan.items.filter((item) => item.type === "video").length;
|
|
276
327
|
if (hasBoardScripts(document))
|
|
277
328
|
scriptHost = createNodeBoardScriptHost(renderer, {
|
|
278
329
|
concurrency: 2,
|
|
@@ -282,18 +333,13 @@ export async function runBoardExport(options) {
|
|
|
282
333
|
},
|
|
283
334
|
});
|
|
284
335
|
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
336
|
const backgroundImage = backgroundTexture
|
|
289
337
|
? { texture: backgroundTexture, fit: declaredBackground.fit ?? "cover", position: "center", opacity: declaredBackground.opacity ?? 1 }
|
|
290
338
|
: undefined;
|
|
291
|
-
const assetKey =
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
: undefined;
|
|
339
|
+
const assetKey = (item) => {
|
|
340
|
+
const key = boardExportAssetKey(item);
|
|
341
|
+
return key && omittedKeys.has(key) ? null : key;
|
|
342
|
+
};
|
|
297
343
|
const frames = [];
|
|
298
344
|
const reported = new Set();
|
|
299
345
|
let completed = 0;
|
|
@@ -320,11 +366,15 @@ export async function runBoardExport(options) {
|
|
|
320
366
|
}
|
|
321
367
|
trace("images", completed, prepared.plan);
|
|
322
368
|
if (options.withImages) {
|
|
323
|
-
const
|
|
369
|
+
const visible = prepared.plan.items.filter((item) => prepared.scene.opacity(item.id) > 0);
|
|
370
|
+
const loaded = await loadBoardTextures(images, options.spaceId, visible, { scale: prepared.plan.scale, maxBytes: imageMemoryBytes, cache: textureCache });
|
|
324
371
|
omittedKeys = new Set(loaded.omitted);
|
|
372
|
+
const sources = (keys) => keys.slice(0, 3).map((key) => boardAssetKeySource(key)?.value ?? key).join(", ");
|
|
373
|
+
const plural = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
325
374
|
const assetWarnings = [
|
|
326
|
-
loaded.failed.length ? `${loaded.failed.length
|
|
327
|
-
loaded.
|
|
375
|
+
loaded.failed.length ? `${plural(loaded.failed.length, "preview")} could not be loaded: ${sources(loaded.failed)}` : null,
|
|
376
|
+
loaded.unavailable.length ? `${plural(loaded.unavailable.length, "video")} drawn without a cover; Node exports take covers from Cohub-hosted media only.` : null,
|
|
377
|
+
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
378
|
];
|
|
329
379
|
for (const warning of assetWarnings)
|
|
330
380
|
if (warning && !reported.has(warning)) {
|
|
@@ -335,12 +385,12 @@ export async function runBoardExport(options) {
|
|
|
335
385
|
trace("render", completed, prepared.plan);
|
|
336
386
|
const result = exportBoardImageBytes(renderer, document, {
|
|
337
387
|
...frameOptions,
|
|
338
|
-
|
|
388
|
+
...theme,
|
|
339
389
|
background: options.background,
|
|
340
390
|
textures,
|
|
341
391
|
scripts: scriptHost ?? undefined,
|
|
342
392
|
backgroundImage,
|
|
343
|
-
|
|
393
|
+
assetKey,
|
|
344
394
|
format: options.format,
|
|
345
395
|
quality: options.quality,
|
|
346
396
|
});
|
|
@@ -387,7 +437,7 @@ function describeWarning(warning) {
|
|
|
387
437
|
}
|
|
388
438
|
if (warning.kind === "images-missing") {
|
|
389
439
|
const keys = warning.keys;
|
|
390
|
-
return `${keys.length}
|
|
440
|
+
return `${keys.length} preview${keys.length === 1 ? "" : "s"} drawn as placeholders; omit --no-images to fetch them.`;
|
|
391
441
|
}
|
|
392
442
|
if (warning.kind === "many-items") {
|
|
393
443
|
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";
|