@neta-art/cohub-cli 8.4.1 → 9.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 +25 -2
- package/dist/board-export.d.ts +21 -36
- package/dist/board-export.js +163 -88
- package/dist/board-kernel.d.ts +2 -3
- package/dist/board-kernel.js +3428 -3770
- package/dist/board-time.d.ts +3 -0
- package/dist/board-time.js +28 -0
- package/dist/commands/apps.js +2 -2
- package/dist/commands/auth.js +2 -2
- package/dist/commands/boards/examples.d.ts +8 -2
- package/dist/commands/boards/examples.js +79 -411
- package/dist/commands/boards.d.ts +4 -9
- package/dist/commands/boards.js +346 -286
- package/dist/commands/desktop.d.ts +1 -5
- package/dist/commands/desktop.js +5 -16
- package/dist/commands/generations.js +1 -1
- package/dist/commands/me.js +1 -1
- package/dist/commands/public.js +3 -3
- package/dist/commands/references.js +3 -0
- package/dist/commands/referrals.js +2 -2
- package/dist/commands/run.js +3 -3
- package/dist/commands/runtime.d.ts +1 -1
- package/dist/commands/runtime.js +12 -8
- package/dist/commands/search.js +15 -7
- package/dist/commands/space-activity.js +3 -5
- package/dist/commands/space-displays.d.ts +2 -0
- package/dist/commands/space-displays.js +347 -0
- package/dist/commands/space-files.d.ts +5 -0
- package/dist/commands/space-files.js +215 -0
- package/dist/commands/space-invitations.js +3 -3
- package/dist/commands/spaces.d.ts +3 -11
- package/dist/commands/spaces.js +146 -75
- package/dist/index.js +7 -4
- package/dist/runtime/launch.d.ts +2 -0
- package/dist/runtime/launch.js +35 -8
- package/dist/runtime/presentation.d.ts +1 -0
- package/dist/runtime/presentation.js +3 -2
- package/dist/runtime/supervisor.d.ts +1 -0
- package/dist/runtime/supervisor.js +2 -1
- package/dist/space-files/limit.d.ts +2 -0
- package/dist/space-files/limit.js +16 -0
- package/dist/space-files/operands.d.ts +16 -0
- package/dist/space-files/operands.js +33 -0
- package/dist/space-files/transfer.d.ts +53 -0
- package/dist/space-files/transfer.js +296 -0
- package/dist/space-files/upload.d.ts +14 -0
- package/dist/space-files/upload.js +53 -0
- package/dist/space.d.ts +18 -16
- package/dist/space.js +86 -39
- package/dist/web.d.ts +1 -0
- package/dist/web.js +6 -0
- package/package.json +2 -2
- package/dist/commands/board-domain.d.ts +0 -2
- package/dist/commands/board-domain.js +0 -16
- package/dist/commands/boards/animation.d.ts +0 -2
- package/dist/commands/boards/animation.js +0 -171
- package/dist/commands/boards/appearance.d.ts +0 -2
- package/dist/commands/boards/appearance.js +0 -127
- package/dist/commands/boards/batch.d.ts +0 -4
- package/dist/commands/boards/batch.js +0 -62
- package/dist/commands/boards/connections.d.ts +0 -2
- package/dist/commands/boards/connections.js +0 -95
- package/dist/commands/boards/context.d.ts +0 -31
- package/dist/commands/boards/context.js +0 -71
- package/dist/commands/boards/items.d.ts +0 -2
- package/dist/commands/boards/items.js +0 -184
- package/dist/commands/boards/transactions.d.ts +0 -12
- package/dist/commands/boards/transactions.js +0 -69
package/README.md
CHANGED
|
@@ -72,12 +72,15 @@ cohub -s <spaceId> spaces invites revoke <code> --yes
|
|
|
72
72
|
cohub -s <spaceId> run -- git status
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
Space-scoped commands use `-s`, then `COHUB_SPACE_ID`, then the Space remembered for
|
|
76
|
+
the current directory. `<space>` is a Space ID, a slug you own such as `home`, or
|
|
77
|
+
`username/slug`. Only `prompt`, `completion`, `generate`, `apps`, and `public` fall
|
|
78
|
+
back to your Home Space; other commands exit with an error when no target is given:
|
|
77
79
|
|
|
78
80
|
```bash
|
|
79
81
|
cohub -s <spaceId> spaces prompt "message" --json
|
|
80
82
|
COHUB_SPACE_ID=<spaceId> cohub spaces prompt "message" --json
|
|
83
|
+
cohub -s home spaces files ls
|
|
81
84
|
```
|
|
82
85
|
|
|
83
86
|
## Local Runtime
|
|
@@ -349,12 +352,32 @@ Pass generation parameters with `--param key=value` or `--parameters '<json>'`.
|
|
|
349
352
|
```bash
|
|
350
353
|
cohub -s <spaceId> spaces files ls [path] --json
|
|
351
354
|
cohub -s <spaceId> spaces files cat <path>
|
|
355
|
+
cohub -s <spaceId> spaces files cat <path> > <local-file>
|
|
352
356
|
cohub -s <spaceId> spaces files write <path> -c "<content>"
|
|
353
357
|
cohub -s <spaceId> spaces files upload <files...> --dir <dir>
|
|
358
|
+
cohub spaces files cp [-r] [-n] [-p] <source>... <destination>
|
|
354
359
|
cohub -s <spaceId> spaces files mv <from> <to>
|
|
355
360
|
cohub -s <spaceId> spaces files rm <path>
|
|
356
361
|
```
|
|
357
362
|
|
|
363
|
+
`cat` streams raw bytes, so redirecting it saves binary files intact.
|
|
364
|
+
|
|
365
|
+
`cp` works like `scp` with `cp` semantics: the last path is the destination,
|
|
366
|
+
directories need `-r`, existing files are overwritten and directories merged
|
|
367
|
+
(`-n` keeps existing files, `-p` keeps modification times). `<space>:<path>`
|
|
368
|
+
addresses a Space by id, `username/slug`, or the slug of a Space you own. Bare
|
|
369
|
+
paths address the current Space when one is declared with `-s` or
|
|
370
|
+
`COHUB_SPACE_ID` (as inside a Cohub sandbox) and local files otherwise;
|
|
371
|
+
absolute paths outside `/workspace` are always local:
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
cohub spaces files cp -r alice/templates:starter ./starter # download
|
|
375
|
+
cohub spaces files cp report.pdf <spaceId>:inbox/ # upload
|
|
376
|
+
cohub -s <spaceId> spaces files cp -r other:docs docs # Space to Space
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Copies between Spaces run server-side and never pass file content through the CLI.
|
|
380
|
+
|
|
358
381
|
`upload` places each file under `--dir`; a directory argument contributes its
|
|
359
382
|
contents directly (like `aws s3 cp dir remote:path`), so `upload dist --dir apps/demo`
|
|
360
383
|
lands at `apps/demo/index.html`, not `apps/demo/dist/index.html`. Upload adds or
|
package/dist/board-export.d.ts
CHANGED
|
@@ -1,45 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
* Board image export for the CLI.
|
|
3
|
-
*
|
|
4
|
-
* The CLI and web editor share canonical Board geometry and card semantics. Each
|
|
5
|
-
* host selects the renderer primitive suited to its backend; this path uses
|
|
6
|
-
* Canvas2D. Everything platform-specific
|
|
7
|
-
* lives here: fetching the document over HTTP, pulling image bytes out of the
|
|
8
|
-
* space, and locating fonts on disk.
|
|
9
|
-
*/
|
|
10
|
-
import type { BoardDocument, BoardExportRegion, BoardItem } from "@neta-art/cohub/board";
|
|
1
|
+
import type { BoardDocument, BoardExportRegion, BoardSceneItem } from "@neta-art/cohub/board";
|
|
11
2
|
import type { BoardHeadlessExportFormat, BoardHeadlessFont, BoardHeadlessRenderer, BoardHeadlessTexture } from "@neta-art/cohub/board/headless";
|
|
12
|
-
/**
|
|
13
|
-
* Geist, as shipped to the browser.
|
|
14
|
-
*
|
|
15
|
-
* The board asks for the "Geist" family; registering the exact same font files
|
|
16
|
-
* the web app loads is what makes CLI output match the editor rather than
|
|
17
|
-
* substituting whatever sans-serif the host happens to have. A source checkout
|
|
18
|
-
* falls back to the dev packages.
|
|
19
|
-
*/
|
|
20
3
|
export declare function resolveBundledFonts(): BoardHeadlessFont[];
|
|
21
4
|
export type BoardExportSource = {
|
|
22
5
|
document: BoardDocument;
|
|
23
6
|
boardId: string;
|
|
24
7
|
title: string | null;
|
|
25
8
|
};
|
|
26
|
-
/**
|
|
27
|
-
* Load a board document by board id or by the path of its `.board` file.
|
|
28
|
-
*
|
|
29
|
-
* A `.board` file is a manifest holding a board id, so both forms converge on
|
|
30
|
-
* the same inspect call — which is also what the web client does.
|
|
31
|
-
*/
|
|
32
9
|
export declare function loadBoardDocument(spaceId: string, target: string): Promise<BoardExportSource>;
|
|
33
|
-
|
|
34
|
-
* Fetch every image in `items`, keyed the way the renderers ask for it.
|
|
35
|
-
*
|
|
36
|
-
* Takes the planned items rather than the whole document so a partial export
|
|
37
|
-
* does not download the rest of the board. Failures are collected rather than
|
|
38
|
-
* thrown: one unreadable image should cost a placeholder and a warning, not the
|
|
39
|
-
* whole export. Downloads run concurrently but bounded, so a board with hundreds
|
|
40
|
-
* of images does not open hundreds of sockets.
|
|
41
|
-
*/
|
|
42
|
-
export declare function loadBoardTextures(headless: BoardHeadlessRenderer, spaceId: string, items: BoardItem[], options?: {
|
|
10
|
+
export declare function loadBoardTextures(headless: BoardHeadlessRenderer, spaceId: string, items: readonly BoardSceneItem[], options?: {
|
|
43
11
|
concurrency?: number;
|
|
44
12
|
}): Promise<{
|
|
45
13
|
textures: Map<string, BoardHeadlessTexture>;
|
|
@@ -50,6 +18,8 @@ export type BoardExportRunOptions = {
|
|
|
50
18
|
spaceId: string;
|
|
51
19
|
target: string;
|
|
52
20
|
region: BoardExportRegion;
|
|
21
|
+
times?: number[];
|
|
22
|
+
animation?: string;
|
|
53
23
|
scale: number;
|
|
54
24
|
padding?: number;
|
|
55
25
|
colorScheme: "dark" | "light";
|
|
@@ -58,14 +28,29 @@ export type BoardExportRunOptions = {
|
|
|
58
28
|
quality?: number;
|
|
59
29
|
withImages: boolean;
|
|
60
30
|
};
|
|
61
|
-
export type
|
|
31
|
+
export type BoardExportFrame = {
|
|
32
|
+
time: number | null;
|
|
62
33
|
bytes: Uint8Array;
|
|
63
34
|
width: number;
|
|
64
35
|
height: number;
|
|
65
36
|
scale: number;
|
|
66
37
|
itemCount: number;
|
|
67
38
|
format: BoardHeadlessExportFormat;
|
|
39
|
+
};
|
|
40
|
+
export type BoardExportRunResult = {
|
|
41
|
+
frames: BoardExportFrame[];
|
|
68
42
|
warnings: string[];
|
|
69
43
|
};
|
|
70
|
-
|
|
44
|
+
export type BoardVideoExportFormat = "mp4" | "webm";
|
|
45
|
+
export type BoardVideoExportOptions = Omit<BoardExportRunOptions, "format"> & {
|
|
46
|
+
format: BoardVideoExportFormat;
|
|
47
|
+
fps: number;
|
|
48
|
+
output: string;
|
|
49
|
+
};
|
|
50
|
+
export declare function runBoardVideoExport(options: BoardVideoExportOptions): Promise<{
|
|
51
|
+
width: number;
|
|
52
|
+
height: number;
|
|
53
|
+
frames: number;
|
|
54
|
+
warnings: string[];
|
|
55
|
+
}>;
|
|
71
56
|
export declare function runBoardExport(options: BoardExportRunOptions): Promise<BoardExportRunResult | null>;
|
package/dist/board-export.js
CHANGED
|
@@ -1,29 +1,17 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Board image export for the CLI.
|
|
3
|
-
*
|
|
4
|
-
* The CLI and web editor share canonical Board geometry and card semantics. Each
|
|
5
|
-
* host selects the renderer primitive suited to its backend; this path uses
|
|
6
|
-
* Canvas2D. Everything platform-specific
|
|
7
|
-
* lives here: fetching the document over HTTP, pulling image bytes out of the
|
|
8
|
-
* space, and locating fonts on disk.
|
|
9
|
-
*/
|
|
10
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
11
5
|
import { createRequire } from "node:module";
|
|
12
6
|
import { dirname, join } from "node:path";
|
|
13
7
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
import {
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
const execFile = promisify(execFileCallback);
|
|
10
|
+
import { boardDocumentAt, boardImageKeySource, buildBoardScene, createBoardHeadlessRenderer, createBoardHeadlessSketchHost, exportBoardImageBytes, imageAssetKey, parseBoardDocument, planBoardExport, selectBoardExportAssets, } from "./board-kernel.js";
|
|
15
11
|
import { resolveBoardId } from "./board-command-support.js";
|
|
16
12
|
import { BOARD_EXPORT_FONTS } from "./board-fonts.js";
|
|
17
13
|
import { createClient } from "./client.js";
|
|
18
14
|
import { downloadPublicImage } from "./safe-remote-image.js";
|
|
19
|
-
/**
|
|
20
|
-
* Geist, as shipped to the browser.
|
|
21
|
-
*
|
|
22
|
-
* The board asks for the "Geist" family; registering the exact same font files
|
|
23
|
-
* the web app loads is what makes CLI output match the editor rather than
|
|
24
|
-
* substituting whatever sans-serif the host happens to have. A source checkout
|
|
25
|
-
* falls back to the dev packages.
|
|
26
|
-
*/
|
|
27
15
|
export function resolveBundledFonts() {
|
|
28
16
|
const require = createRequire(import.meta.url);
|
|
29
17
|
const bundled = fileURLToPath(new URL("./fonts/", import.meta.url));
|
|
@@ -44,33 +32,21 @@ export function resolveBundledFonts() {
|
|
|
44
32
|
return path ? [{ path, family: font.family }] : [];
|
|
45
33
|
});
|
|
46
34
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Load a board document by board id or by the path of its `.board` file.
|
|
49
|
-
*
|
|
50
|
-
* A `.board` file is a manifest holding a board id, so both forms converge on
|
|
51
|
-
* the same inspect call — which is also what the web client does.
|
|
52
|
-
*/
|
|
53
35
|
export async function loadBoardDocument(spaceId, target) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
36
|
+
const board = createClient().space(spaceId).board(await resolveBoardId(spaceId, target));
|
|
37
|
+
let result = await board.get();
|
|
38
|
+
const items = { ...result.items };
|
|
39
|
+
while (result.next) {
|
|
40
|
+
result = await board.get({ only: ["items"], cursor: result.next });
|
|
41
|
+
Object.assign(items, result.items);
|
|
42
|
+
}
|
|
43
|
+
const parsed = parseBoardDocument({ board: result.board, items, animations: result.animations ?? {} });
|
|
44
|
+
if (!parsed.ok)
|
|
45
|
+
throw new Error(`Board ${result.id} could not be read: ${parsed.diagnostics[0]?.path}: ${parsed.diagnostics[0]?.message}`);
|
|
46
|
+
return { document: parsed.document, boardId: result.id, title: result.title };
|
|
62
47
|
}
|
|
63
|
-
/**
|
|
64
|
-
* Fetch every image in `items`, keyed the way the renderers ask for it.
|
|
65
|
-
*
|
|
66
|
-
* Takes the planned items rather than the whole document so a partial export
|
|
67
|
-
* does not download the rest of the board. Failures are collected rather than
|
|
68
|
-
* thrown: one unreadable image should cost a placeholder and a warning, not the
|
|
69
|
-
* whole export. Downloads run concurrently but bounded, so a board with hundreds
|
|
70
|
-
* of images does not open hundreds of sockets.
|
|
71
|
-
*/
|
|
72
48
|
export async function loadBoardTextures(headless, spaceId, items, options = {}) {
|
|
73
|
-
const selection = selectBoardExportAssets(items, imageAssetKey);
|
|
49
|
+
const selection = selectBoardExportAssets([...items], imageAssetKey);
|
|
74
50
|
const textures = new Map();
|
|
75
51
|
const failed = [];
|
|
76
52
|
const pending = [...selection.keys];
|
|
@@ -105,13 +81,80 @@ async function readSpaceFileBytes(client, spaceId, path) {
|
|
|
105
81
|
const { blob, mimeType } = await client.space(spaceId).files.download(path);
|
|
106
82
|
return { bytes: new Uint8Array(await blob.arrayBuffer()), mimeType };
|
|
107
83
|
}
|
|
108
|
-
|
|
84
|
+
export async function runBoardVideoExport(options) {
|
|
85
|
+
if (!options.animation)
|
|
86
|
+
throw new Error("Video export needs --animation.");
|
|
87
|
+
if (!options.times || options.times.length < 2)
|
|
88
|
+
throw new Error("Video export needs an --at range with at least two frames.");
|
|
89
|
+
const times = options.times;
|
|
90
|
+
const step = times[1] - times[0];
|
|
91
|
+
if (!Number.isFinite(step) || step <= 0)
|
|
92
|
+
throw new Error("Video export needs an increasing --at range.");
|
|
93
|
+
const derivedFps = 1000 / step;
|
|
94
|
+
if (Math.abs(options.fps - derivedFps) > 0.01)
|
|
95
|
+
throw new Error(`--fps must match the --at step (${derivedFps.toFixed(3)} fps).`);
|
|
96
|
+
const result = await runBoardExport({ ...options, format: "png" });
|
|
97
|
+
if (!result)
|
|
98
|
+
throw new Error("Nothing to export: the region contains no items.");
|
|
99
|
+
const tempDir = await mkdtemp(join(tmpdir(), "cohub-board-export-"));
|
|
100
|
+
try {
|
|
101
|
+
for (const [index, frame] of result.frames.entries())
|
|
102
|
+
await writeFile(join(tempDir, `${String(index + 1).padStart(6, "0")}.png`), frame.bytes);
|
|
103
|
+
const document = (await loadBoardDocument(options.spaceId, options.target)).document;
|
|
104
|
+
const audioDocument = boardDocumentAt(document, options.animation, times[0]).document;
|
|
105
|
+
const audio = Object.values(audioDocument.items).filter((item) => item.type === "audio");
|
|
106
|
+
const audioInputs = [];
|
|
107
|
+
for (const [index, item] of audio.entries()) {
|
|
108
|
+
const { bytes } = await readSpaceFileBytes(createClient(), options.spaceId, item.props.src);
|
|
109
|
+
const path = join(tempDir, `audio-${index}-${item.props.src.split("/").pop() ?? "track"}`);
|
|
110
|
+
await writeFile(path, bytes);
|
|
111
|
+
audioInputs.push({ path, offset: Math.max(0, item.props.time ?? 0) / 1000 });
|
|
112
|
+
}
|
|
113
|
+
const duration = Math.max(0.001, result.frames.length / options.fps);
|
|
114
|
+
const args = ["-y", "-f", "image2", "-framerate", String(options.fps), "-i", join(tempDir, "%06d.png")];
|
|
115
|
+
for (const input of audioInputs) {
|
|
116
|
+
if (input.offset > 0)
|
|
117
|
+
args.push("-ss", input.offset.toFixed(3));
|
|
118
|
+
args.push("-i", input.path);
|
|
119
|
+
}
|
|
120
|
+
if (audioInputs.length > 1) {
|
|
121
|
+
const inputs = audioInputs.map((_, index) => `[${index + 1}:a]`).join("");
|
|
122
|
+
args.push("-filter_complex", `${inputs}amix=inputs=${audioInputs.length}:duration=longest:dropout_transition=0[aout]`, "-map", "0:v:0", "-map", "[aout]");
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
args.push("-map", "0:v:0");
|
|
126
|
+
if (audioInputs.length === 1)
|
|
127
|
+
args.push("-map", "1:a:0");
|
|
128
|
+
}
|
|
129
|
+
if (options.format === "mp4")
|
|
130
|
+
args.push("-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac", "-movflags", "+faststart", "-f", "mp4");
|
|
131
|
+
else
|
|
132
|
+
args.push("-c:v", "libvpx-vp9", "-crf", "32", "-b:v", "0", "-c:a", "libopus", "-f", "webm");
|
|
133
|
+
args.push("-t", duration.toFixed(3), options.output);
|
|
134
|
+
try {
|
|
135
|
+
await execFile("ffmpeg", args, { encoding: "utf8", maxBuffer: 2 * 1024 * 1024 });
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
const cause = error;
|
|
139
|
+
if (cause.code === "ENOENT")
|
|
140
|
+
throw new Error("Video export needs ffmpeg. Install ffmpeg and try again.");
|
|
141
|
+
throw new Error(`ffmpeg could not encode the video: ${(cause.stderr ?? cause.message ?? "unknown error").trim().split("\n").slice(-6).join("\n")}`);
|
|
142
|
+
}
|
|
143
|
+
return { width: result.frames[0]?.width ?? 0, height: result.frames[0]?.height ?? 0, frames: result.frames.length, warnings: result.warnings };
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
109
149
|
export async function runBoardExport(options) {
|
|
110
150
|
const { document } = await loadBoardDocument(options.spaceId, options.target);
|
|
111
|
-
|
|
112
|
-
|
|
151
|
+
if (options.animation && !document.animations[options.animation]) {
|
|
152
|
+
throw new Error(`Animation ${options.animation} does not exist.`);
|
|
153
|
+
}
|
|
154
|
+
const times = options.times?.length ? options.times : [null];
|
|
155
|
+
const documentAt = (time) => time === null ? document : boardDocumentAt(document, options.animation ?? null, time).document;
|
|
113
156
|
const plan = planBoardExport({
|
|
114
|
-
|
|
157
|
+
scene: buildBoardScene(documentAt(times[0] ?? null)),
|
|
115
158
|
region: options.region,
|
|
116
159
|
scale: options.scale,
|
|
117
160
|
...(options.padding === undefined ? {} : { padding: options.padding }),
|
|
@@ -119,6 +162,7 @@ export async function runBoardExport(options) {
|
|
|
119
162
|
if (!plan)
|
|
120
163
|
return null;
|
|
121
164
|
const headless = await createBoardHeadlessRenderer({ fonts: resolveBundledFonts() });
|
|
165
|
+
let sketchHost = null;
|
|
122
166
|
try {
|
|
123
167
|
const warnings = [];
|
|
124
168
|
let textures;
|
|
@@ -135,7 +179,7 @@ export async function runBoardExport(options) {
|
|
|
135
179
|
warnings.push(`${loaded.omitted.length} previews were drawn as placeholders to stay within the export texture limit.`);
|
|
136
180
|
}
|
|
137
181
|
}
|
|
138
|
-
const declaredBackground = document.
|
|
182
|
+
const declaredBackground = document.board.background;
|
|
139
183
|
if (options.withImages &&
|
|
140
184
|
options.background === "paper" &&
|
|
141
185
|
declaredBackground.kind === "image" &&
|
|
@@ -149,54 +193,85 @@ export async function runBoardExport(options) {
|
|
|
149
193
|
}
|
|
150
194
|
}
|
|
151
195
|
const videoCount = plan.items.filter((item) => item.type === "video").length;
|
|
196
|
+
sketchHost = createBoardHeadlessSketchHost(headless, {
|
|
197
|
+
readModule: async (src) => {
|
|
198
|
+
const { bytes } = await readSpaceFileBytes(createClient(), options.spaceId, src);
|
|
199
|
+
return new TextDecoder().decode(bytes);
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
const region = times.length > 1 ? { kind: "rect", rect: plan.world } : options.region;
|
|
203
|
+
const host = sketchHost;
|
|
204
|
+
if (!host)
|
|
205
|
+
throw new Error("Headless sketch host could not be created.");
|
|
206
|
+
const sketchVariants = new Map();
|
|
207
|
+
for (const time of times) {
|
|
208
|
+
const sketchPlan = planBoardExport({
|
|
209
|
+
scene: buildBoardScene(documentAt(time)),
|
|
210
|
+
region,
|
|
211
|
+
scale: options.scale,
|
|
212
|
+
...(times.length > 1 ? { padding: 0 } : options.padding === undefined ? {} : { padding: options.padding }),
|
|
213
|
+
});
|
|
214
|
+
for (const item of sketchPlan?.items.filter((entry) => entry.type === "sketch") ?? []) {
|
|
215
|
+
const key = `${item.id}:${item.frame.width}:${item.frame.height}:${item.props.src}:${item.props.seed ?? ""}:${JSON.stringify(item.props.params)}`;
|
|
216
|
+
const variant = sketchVariants.get(key) ?? { item, times: [] };
|
|
217
|
+
variant.times.push(time ?? 0);
|
|
218
|
+
sketchVariants.set(key, variant);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
await Promise.all([...sketchVariants.values()].map((variant) => host.prepare([variant.item], variant.times, plan.scale)));
|
|
152
222
|
if (videoCount > 0) {
|
|
153
223
|
warnings.push(`${videoCount} video preview${videoCount === 1 ? " was" : "s were"} drawn as placeholders; headless video decoding is unavailable.`);
|
|
154
224
|
}
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
?
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
: {}),
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return null;
|
|
183
|
-
for (const warning of result.warnings) {
|
|
184
|
-
// Missing-image warnings are already reported above with their paths.
|
|
185
|
-
if (warning.kind === "images-missing" && options.withImages)
|
|
225
|
+
const backgroundImage = backgroundTexture
|
|
226
|
+
? { texture: backgroundTexture, fit: declaredBackground.fit ?? "cover", position: "center", opacity: declaredBackground.opacity ?? 1 }
|
|
227
|
+
: undefined;
|
|
228
|
+
const assetKey = options.withImages
|
|
229
|
+
? (item) => {
|
|
230
|
+
const key = imageAssetKey(item);
|
|
231
|
+
return key && omittedKeys.has(key) ? null : key;
|
|
232
|
+
}
|
|
233
|
+
: undefined;
|
|
234
|
+
const frames = [];
|
|
235
|
+
const reported = new Set();
|
|
236
|
+
for (const time of times) {
|
|
237
|
+
const result = exportBoardImageBytes(headless, document, {
|
|
238
|
+
...(time === null ? {} : { at: { ...(options.animation ? { animation: options.animation } : {}), time } }),
|
|
239
|
+
region,
|
|
240
|
+
scale: options.scale,
|
|
241
|
+
padding: times.length > 1 ? 0 : options.padding,
|
|
242
|
+
colorScheme: options.colorScheme,
|
|
243
|
+
background: options.background,
|
|
244
|
+
textures,
|
|
245
|
+
sketches: sketchHost ?? undefined,
|
|
246
|
+
backgroundImage,
|
|
247
|
+
...(assetKey ? { assetKey } : {}),
|
|
248
|
+
format: options.format,
|
|
249
|
+
quality: options.quality,
|
|
250
|
+
});
|
|
251
|
+
if (!result)
|
|
186
252
|
continue;
|
|
187
|
-
|
|
253
|
+
for (const warning of result.warnings) {
|
|
254
|
+
if (warning.kind === "images-missing" && options.withImages)
|
|
255
|
+
continue;
|
|
256
|
+
const text = describeWarning(warning);
|
|
257
|
+
if (!reported.has(text))
|
|
258
|
+
warnings.push(text);
|
|
259
|
+
reported.add(text);
|
|
260
|
+
}
|
|
261
|
+
frames.push({
|
|
262
|
+
time,
|
|
263
|
+
bytes: result.bytes,
|
|
264
|
+
width: result.plan.width,
|
|
265
|
+
height: result.plan.height,
|
|
266
|
+
scale: result.plan.scale,
|
|
267
|
+
itemCount: result.plan.items.length,
|
|
268
|
+
format: result.format,
|
|
269
|
+
});
|
|
188
270
|
}
|
|
189
|
-
return {
|
|
190
|
-
bytes: result.bytes,
|
|
191
|
-
width: result.plan.width,
|
|
192
|
-
height: result.plan.height,
|
|
193
|
-
scale: result.plan.scale,
|
|
194
|
-
itemCount: result.plan.items.length,
|
|
195
|
-
format: result.format,
|
|
196
|
-
warnings,
|
|
197
|
-
};
|
|
271
|
+
return frames.length ? { frames, warnings } : null;
|
|
198
272
|
}
|
|
199
273
|
finally {
|
|
274
|
+
sketchHost?.destroy();
|
|
200
275
|
headless.destroy();
|
|
201
276
|
}
|
|
202
277
|
}
|
package/dist/board-kernel.d.ts
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export {
|
|
3
|
-
export { createBoardHeadlessRenderer, exportBoardImageBytes } from "@neta-art/cohub/board/headless";
|
|
1
|
+
export { boardDocumentAt, boardImageKeySource, buildBoardScene, imageAssetKey, parseBoardDocument, planBoardExport, selectBoardExportAssets, } from "@neta-art/cohub/board";
|
|
2
|
+
export { createBoardHeadlessRenderer, createBoardHeadlessSketchHost, exportBoardImageBytes } from "@neta-art/cohub/board/headless";
|