@neta-art/cohub-cli 10.3.0 → 10.4.1
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 +30 -0
- package/dist/board-export-resources.d.ts +8 -0
- package/dist/board-export-resources.js +39 -0
- package/dist/board-export.d.ts +6 -0
- package/dist/board-export.js +58 -8
- package/dist/board-kernel.d.ts +1 -1
- package/dist/board-kernel.js +109 -39
- package/dist/commands/boards/authoring.js +14 -16
- package/dist/commands/boards.d.ts +1 -0
- package/dist/commands/boards.js +27 -1
- package/dist/commands/sandboxd-binary.d.ts +3 -3
- package/dist/commands/sandboxd-binary.js +27 -17
- package/dist/runtime/harness.d.ts +5 -0
- package/dist/runtime/harness.js +29 -3
- package/dist/runtime/json-rpc.js +1 -1
- package/dist/runtime/launch.js +21 -6
- package/dist/runtime/native/attach.js +5 -2
- package/dist/runtime/native/codex-link.d.ts +2 -0
- package/dist/runtime/native/codex-link.js +3 -1
- package/dist/runtime/native/execution.js +3 -3
- package/dist/runtime/native/ingest.js +4 -4
- package/dist/runtime/native/paths.d.ts +2 -0
- package/dist/runtime/native/paths.js +7 -0
- package/dist/runtime/native/pi-extension.js +12 -2
- package/dist/runtime/native/pi-link.js +21 -4
- package/dist/runtime/native/version.js +4 -1
- package/dist/runtime/supervisor.js +33 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -55,6 +55,36 @@ and, if needed, `COHUB_AUTH_RESOURCE` and `COHUB_AUTH_SCOPE`. Stored login sessi
|
|
|
55
55
|
Home Space caches, and Runtime directory bindings are scoped to the API base URL.
|
|
56
56
|
Sign in once for each custom server, or supply `COHUB_EXECUTION_TOKEN`.
|
|
57
57
|
|
|
58
|
+
## Board export resources
|
|
59
|
+
|
|
60
|
+
Board exports keep the requested resolution and source image quality. Before rendering,
|
|
61
|
+
`boards export` prints the output dimensions and frame count (except with `--json`).
|
|
62
|
+
Decoded image textures, including background and script output images, share a 256 MiB
|
|
63
|
+
budget. Oversized images fail before native decoding rather than silently becoming
|
|
64
|
+
placeholders. Use `--image-memory <MiB>` to raise the budget when the environment has
|
|
65
|
+
sufficient memory; it is an image budget, not a limit on total process memory.
|
|
66
|
+
`--size` controls the viewport only with `--camera`.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
cohub boards export lesson.board --animation lecture --at 0:92s:40ms --camera --size 1600x900 -o lecture.mp4
|
|
70
|
+
NODE_DEBUG=cohub:board-export cohub boards export lesson.board --animation lecture --at 0:92s:40ms -o lecture.mp4
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Debug output reports render stages, RSS, heap, external buffers, reserved image bytes,
|
|
74
|
+
and available/constrained process memory. Video encoding is reported separately.
|
|
75
|
+
MP4 exports require even output dimensions and fail before rendering if the initial
|
|
76
|
+
export dimensions are odd. Video frame dimensions must remain constant.
|
|
77
|
+
|
|
78
|
+
For a repeatable 2,301-frame native export memory benchmark, run from the repository root:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
node --import tsx packages/cli/scripts/bench-board-export.ts
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Set `BOARD_EXPORT_BENCH_FRAMES`, `BOARD_EXPORT_BENCH_WIDTH`, or
|
|
85
|
+
`BOARD_EXPORT_BENCH_HEIGHT` to change the workload. The benchmark discards PNGs
|
|
86
|
+
as they are produced and stops if RSS exceeds 1 GiB.
|
|
87
|
+
|
|
58
88
|
## Usage
|
|
59
89
|
|
|
60
90
|
Use `--json` when reading output for decisions, extracting IDs, or chaining commands.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BoardNodeRenderer } from "@neta-art/cohub/board/export/node";
|
|
2
|
+
export declare const BOARD_EXPORT_IMAGE_MEMORY_MIB = 256;
|
|
3
|
+
export declare class BoardExportResourceError extends Error {
|
|
4
|
+
}
|
|
5
|
+
export declare function createBoardExportImageBudget(renderer: BoardNodeRenderer, maxBytes: number): {
|
|
6
|
+
decodeImage: (bytes: Uint8Array, mimeType?: string) => Promise<import("@neta-art/cohub/board/export/node").BoardNodeTexture>;
|
|
7
|
+
readonly usedBytes: number;
|
|
8
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const BOARD_EXPORT_IMAGE_MEMORY_MIB = 256;
|
|
2
|
+
export class BoardExportResourceError extends Error {
|
|
3
|
+
}
|
|
4
|
+
export function createBoardExportImageBudget(renderer, maxBytes) {
|
|
5
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)
|
|
6
|
+
throw new Error("Image memory budget must be a positive integer.");
|
|
7
|
+
let usedBytes = 0;
|
|
8
|
+
const decodeImage = async (bytes, mimeType) => {
|
|
9
|
+
const { default: sharp } = await import("sharp");
|
|
10
|
+
const { width, height } = await sharp(bytes, { limitInputPixels: false }).metadata();
|
|
11
|
+
const required = (width ?? 0) * (height ?? 0) * 4;
|
|
12
|
+
if (!Number.isSafeInteger(required) || required <= 0)
|
|
13
|
+
throw new Error("Image dimensions could not be read.");
|
|
14
|
+
if (required > maxBytes - usedBytes) {
|
|
15
|
+
throw new BoardExportResourceError(`Image ${width}x${height} needs ${Math.ceil(required / 1024 ** 2)} MiB; ` +
|
|
16
|
+
`${Math.ceil(usedBytes / 1024 ** 2)} MiB is already reserved against the ${Math.floor(maxBytes / 1024 ** 2)} MiB image budget. ` +
|
|
17
|
+
"Use smaller source images, export a smaller region, or increase --image-memory if enough memory is available.");
|
|
18
|
+
}
|
|
19
|
+
usedBytes += required;
|
|
20
|
+
try {
|
|
21
|
+
const texture = await renderer.decodeImage(bytes, mimeType);
|
|
22
|
+
const destroy = texture.destroy.bind(texture);
|
|
23
|
+
let released = false;
|
|
24
|
+
texture.destroy = (destroySource) => {
|
|
25
|
+
if (destroySource && !released) {
|
|
26
|
+
released = true;
|
|
27
|
+
usedBytes -= required;
|
|
28
|
+
}
|
|
29
|
+
destroy(destroySource);
|
|
30
|
+
};
|
|
31
|
+
return texture;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
usedBytes -= required;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
return { decodeImage, get usedBytes() { return usedBytes; } };
|
|
39
|
+
}
|
package/dist/board-export.d.ts
CHANGED
|
@@ -38,6 +38,12 @@ export type BoardExportRunOptions = {
|
|
|
38
38
|
};
|
|
39
39
|
document?: BoardDocument;
|
|
40
40
|
signal?: AbortSignal;
|
|
41
|
+
imageMemoryMiB?: number;
|
|
42
|
+
onPlan?: (plan: {
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
frames: number;
|
|
46
|
+
}) => void;
|
|
41
47
|
onFrame?: (frame: BoardExportFrame, index: number) => Promise<void>;
|
|
42
48
|
onProgress?: (completed: number, total: number) => void;
|
|
43
49
|
};
|
package/dist/board-export.js
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdtemp, rm, writeFile, copyFile, link, rename, unlink } from "node:fs/
|
|
|
3
3
|
import { constants } from "node:fs";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { execFile as execFileCallback } from "node:child_process";
|
|
6
|
-
import { promisify } from "node:util";
|
|
6
|
+
import { debuglog, promisify } from "node:util";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
@@ -15,6 +15,8 @@ import { resolveBoardId } from "./board-command-support.js";
|
|
|
15
15
|
import { BOARD_EXPORT_FONTS } from "./board-fonts.js";
|
|
16
16
|
import { createClient } from "./client.js";
|
|
17
17
|
import { downloadPublicAudio, downloadPublicImage } from "./safe-remote-image.js";
|
|
18
|
+
import { BOARD_EXPORT_IMAGE_MEMORY_MIB, BoardExportResourceError, createBoardExportImageBudget } from "./board-export-resources.js";
|
|
19
|
+
const debugExport = debuglog("cohub:board-export");
|
|
18
20
|
export function resolveBundledFonts() {
|
|
19
21
|
const require = createRequire(import.meta.url);
|
|
20
22
|
const bundled = fileURLToPath(new URL("./fonts/", import.meta.url));
|
|
@@ -66,10 +68,13 @@ export async function loadBoardTextures(renderer, spaceId, items, options = {})
|
|
|
66
68
|
if (!wanted.has(key))
|
|
67
69
|
failed.delete(key);
|
|
68
70
|
const pending = selection.keys.filter((key) => !textures.has(key) && !failed.has(key));
|
|
69
|
-
const concurrency = Math.max(1, Math.min(options.concurrency ??
|
|
71
|
+
const concurrency = Math.max(1, Math.min(options.concurrency ?? 2, 16));
|
|
70
72
|
const client = createClient();
|
|
73
|
+
let resourceError;
|
|
71
74
|
async function worker() {
|
|
72
75
|
for (;;) {
|
|
76
|
+
if (resourceError)
|
|
77
|
+
return;
|
|
73
78
|
const key = pending.shift();
|
|
74
79
|
if (!key)
|
|
75
80
|
return;
|
|
@@ -85,12 +90,17 @@ export async function loadBoardTextures(renderer, spaceId, items, options = {})
|
|
|
85
90
|
const texture = await renderer.decodeImage(bytes, mimeType);
|
|
86
91
|
textures.set(key, texture);
|
|
87
92
|
}
|
|
88
|
-
catch {
|
|
89
|
-
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error instanceof BoardExportResourceError)
|
|
95
|
+
resourceError = error;
|
|
96
|
+
else
|
|
97
|
+
failed.add(key);
|
|
90
98
|
}
|
|
91
99
|
}
|
|
92
100
|
}
|
|
93
101
|
await Promise.all(Array.from({ length: Math.min(concurrency, pending.length) }, worker));
|
|
102
|
+
if (resourceError)
|
|
103
|
+
throw resourceError;
|
|
94
104
|
return { textures, failed: [...failed], omitted: selection.omittedKeys };
|
|
95
105
|
}
|
|
96
106
|
async function readSpaceFileBytes(client, spaceId, path) {
|
|
@@ -117,7 +127,14 @@ export async function runBoardVideoExport(options) {
|
|
|
117
127
|
try {
|
|
118
128
|
let count = 0;
|
|
119
129
|
let dimensions = { width: 0, height: 0 };
|
|
120
|
-
const result = await runBoardExport({ ...options, document, format: "png",
|
|
130
|
+
const result = await runBoardExport({ ...options, document, format: "png", onPlan: (plan) => {
|
|
131
|
+
if (options.format === "mp4" && (plan.width % 2 || plan.height % 2)) {
|
|
132
|
+
throw new Error(`MP4 needs even dimensions, but the export is ${plan.width}x${plan.height}. Use --camera --size with even dimensions, or adjust the export region.`);
|
|
133
|
+
}
|
|
134
|
+
options.onPlan?.(plan);
|
|
135
|
+
}, onFrame: async (frame, index) => {
|
|
136
|
+
if (count && (frame.width !== dimensions.width || frame.height !== dimensions.height))
|
|
137
|
+
throw new Error("Video frame dimensions changed. Use a fixed export region or a smaller camera viewport.");
|
|
121
138
|
await writeFile(join(tempDir, `${String(index + 1).padStart(6, "0")}.png`), frame.bytes, { signal: options.signal });
|
|
122
139
|
dimensions = { width: frame.width, height: frame.height };
|
|
123
140
|
count++;
|
|
@@ -160,6 +177,7 @@ export async function runBoardVideoExport(options) {
|
|
|
160
177
|
args.push("-c:v", "libvpx-vp9", "-crf", "32", "-b:v", "0", "-c:a", "libopus", "-f", "webm");
|
|
161
178
|
const encoded = join(tempDir, `video.${options.format}`);
|
|
162
179
|
args.push("-t", duration.toFixed(3), encoded);
|
|
180
|
+
debugExport("%j", { stage: "encode", width: dimensions.width, height: dimensions.height, frames: count, memory: process.memoryUsage() });
|
|
163
181
|
try {
|
|
164
182
|
await execFile("ffmpeg", args, { encoding: "utf8", maxBuffer: 2 * 1024 * 1024, signal: options.signal });
|
|
165
183
|
}
|
|
@@ -195,7 +213,22 @@ export async function runBoardExport(options) {
|
|
|
195
213
|
throw new Error(`Animation ${options.animation} does not exist.`);
|
|
196
214
|
}
|
|
197
215
|
const times = options.times?.length ? options.times : [null];
|
|
198
|
-
const
|
|
216
|
+
const imageMemoryBytes = (options.imageMemoryMiB ?? BOARD_EXPORT_IMAGE_MEMORY_MIB) * 1024 ** 2;
|
|
217
|
+
if (!Number.isSafeInteger(imageMemoryBytes) || imageMemoryBytes <= 0)
|
|
218
|
+
throw new Error("Image memory budget must be a positive integer.");
|
|
219
|
+
const nodeRenderer = await createNodeBoardRenderer({ fonts: resolveBundledFonts() });
|
|
220
|
+
const images = createBoardExportImageBudget(nodeRenderer, imageMemoryBytes);
|
|
221
|
+
const renderer = { ...nodeRenderer, decodeImage: images.decodeImage };
|
|
222
|
+
const trace = (stage, completed, dimensions) => {
|
|
223
|
+
if (!debugExport.enabled || completed % 30 !== 0)
|
|
224
|
+
return;
|
|
225
|
+
debugExport("%j", {
|
|
226
|
+
stage, completed, total: times.length,
|
|
227
|
+
...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),
|
|
228
|
+
imageBytes: images.usedBytes, memory: process.memoryUsage(),
|
|
229
|
+
availableMemory: process.availableMemory(), constrainedMemory: process.constrainedMemory(),
|
|
230
|
+
});
|
|
231
|
+
};
|
|
199
232
|
let scriptHost = null;
|
|
200
233
|
const textureCache = { textures: new Map(), failed: new Set() };
|
|
201
234
|
let backgroundTexture;
|
|
@@ -210,6 +243,17 @@ export async function runBoardExport(options) {
|
|
|
210
243
|
});
|
|
211
244
|
if (!plan)
|
|
212
245
|
return null;
|
|
246
|
+
const region = times.length > 1 && !options.camera ? { kind: "rect", rect: plan.world } : options.region;
|
|
247
|
+
const firstTime = times[0] ?? null;
|
|
248
|
+
const first = prepareBoardExport(document, {
|
|
249
|
+
sampler, region, camera: options.camera, scale: options.scale,
|
|
250
|
+
padding: times.length > 1 ? 0 : options.padding,
|
|
251
|
+
...(firstTime === null ? {} : { at: { animation: options.animation, time: firstTime } }),
|
|
252
|
+
});
|
|
253
|
+
if (!first)
|
|
254
|
+
return null;
|
|
255
|
+
options.onPlan?.({ width: first.plan.width, height: first.plan.height, frames: times.length });
|
|
256
|
+
trace("plan", 0, first.plan);
|
|
213
257
|
const warnings = [];
|
|
214
258
|
const textures = textureCache.textures;
|
|
215
259
|
let omittedKeys = new Set();
|
|
@@ -222,19 +266,21 @@ export async function runBoardExport(options) {
|
|
|
222
266
|
const { bytes, mimeType } = await downloadPublicImage(declaredBackground.imageUrl);
|
|
223
267
|
backgroundTexture = await renderer.decodeImage(bytes, mimeType);
|
|
224
268
|
}
|
|
225
|
-
catch {
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (error instanceof BoardExportResourceError)
|
|
271
|
+
throw error;
|
|
226
272
|
warnings.push("The board background image could not be loaded; the fallback color was exported.");
|
|
227
273
|
}
|
|
228
274
|
}
|
|
229
275
|
const videoCount = plan.items.filter((item) => item.type === "video").length;
|
|
230
276
|
if (hasBoardScripts(document))
|
|
231
277
|
scriptHost = createNodeBoardScriptHost(renderer, {
|
|
278
|
+
concurrency: 2,
|
|
232
279
|
readModule: async (src) => {
|
|
233
280
|
const { bytes } = await readSpaceFileBytes(createClient(), options.spaceId, src);
|
|
234
281
|
return new TextDecoder().decode(bytes);
|
|
235
282
|
},
|
|
236
283
|
});
|
|
237
|
-
const region = times.length > 1 && !options.camera ? { kind: "rect", rect: plan.world } : options.region;
|
|
238
284
|
const host = scriptHost;
|
|
239
285
|
if (videoCount > 0) {
|
|
240
286
|
warnings.push(`${videoCount} video preview${videoCount === 1 ? " was" : "s were"} drawn as placeholders; video decoding is unavailable in Node.`);
|
|
@@ -259,6 +305,7 @@ export async function runBoardExport(options) {
|
|
|
259
305
|
if (!initial)
|
|
260
306
|
continue;
|
|
261
307
|
let prepared = initial;
|
|
308
|
+
trace("prepare", completed, initial.plan);
|
|
262
309
|
if (host) {
|
|
263
310
|
await host.prepare(initial.scene.items, [time ?? 0], initial.plan.scale);
|
|
264
311
|
for (const item of initial.scene.items) {
|
|
@@ -271,6 +318,7 @@ export async function runBoardExport(options) {
|
|
|
271
318
|
continue;
|
|
272
319
|
prepared = scripted;
|
|
273
320
|
}
|
|
321
|
+
trace("images", completed, prepared.plan);
|
|
274
322
|
if (options.withImages) {
|
|
275
323
|
const loaded = await loadBoardTextures(renderer, options.spaceId, prepared.plan.items.filter((item) => prepared.scene.opacity(item.id) > 0), { cache: textureCache });
|
|
276
324
|
omittedKeys = new Set(loaded.omitted);
|
|
@@ -284,6 +332,7 @@ export async function runBoardExport(options) {
|
|
|
284
332
|
reported.add(warning);
|
|
285
333
|
}
|
|
286
334
|
}
|
|
335
|
+
trace("render", completed, prepared.plan);
|
|
287
336
|
const result = exportBoardImageBytes(renderer, document, {
|
|
288
337
|
...frameOptions,
|
|
289
338
|
colorScheme: options.colorScheme,
|
|
@@ -320,6 +369,7 @@ export async function runBoardExport(options) {
|
|
|
320
369
|
frames.push(frame);
|
|
321
370
|
completed++;
|
|
322
371
|
options.onProgress?.(completed, times.length);
|
|
372
|
+
trace("rendered", completed, result.plan);
|
|
323
373
|
}
|
|
324
374
|
return completed ? { frames, warnings } : null;
|
|
325
375
|
}
|
package/dist/board-kernel.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { BOARD_SCRIPT_DRAWING_EXAMPLE, hasBoardScripts, boardDocumentAt, createBoardAnimationSampler, boardSequence, boardSequencePatch, boardPathTrack, boardImageKeySource, buildBoardScene, imageAssetKey, parseBoardDocument, planBoardExport, selectBoardExportAssets, } from "@neta-art/cohub/board";
|
|
2
2
|
export { prepareBoardExport } from "@neta-art/cohub/board/export";
|
|
3
|
-
export { createBoardRenderSampler, inspectBoardScene, measureBoardText } from "@neta-art/cohub/board/render";
|
|
3
|
+
export { createBoardRenderSampler, inspectBoardScene, inspectBoardItems, measureBoardText } from "@neta-art/cohub/board/render";
|
|
4
4
|
export { createNodeBoardRenderer, createNodeBoardScriptHost, exportBoardImageBytes } from "@neta-art/cohub/board/export/node";
|
package/dist/board-kernel.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as groupD8, A as bgr2rgb, B as Bounds, C as Sprite, D as TexturePool, E as RenderGroup, G as TextureStyle, H as Texture, I as BigPool, J as deprecation, K as definedProps, L as GlobalResourceRegistry, M as multiplyHexColors, N as getGlobalBounds, O as InstructionSet, P as FilterEffect, Q as Rectangle, R as warn, S as DOMAdapter, T as Container, U as TextureMatrix, V as updateQuadBounds, W as TextureSource, X as v8_3_4, Y as v8_0_0, Z as uid$1, _ as Cache, a as Geometry, at as extensions, b as ImageSource, c as Filter, d as RendererType, et as Matrix, f as BindGroup, g as getTestContext, h as GlProgram, i as canUseNewCanvasBlendModes, it as ExtensionType, j as multiplyColors, k as getLocalBounds, l as State, m as GpuProgram, n as CanvasPool, nt as eventemitter3_default, o as Buffer
|
|
1
|
+
import { $ as groupD8, A as bgr2rgb, B as Bounds, C as Sprite, D as TexturePool, E as RenderGroup, G as TextureStyle, H as Texture, I as BigPool, J as deprecation, K as definedProps, L as GlobalResourceRegistry, M as multiplyHexColors, N as getGlobalBounds, O as InstructionSet, P as FilterEffect, Q as Rectangle, R as warn, S as DOMAdapter, T as Container, U as TextureMatrix, V as updateQuadBounds, W as TextureSource, X as v8_3_4, Y as v8_0_0, Z as uid$1, _ as Cache, a as Geometry, at as extensions, b as ImageSource, c as Filter, d as RendererType, et as Matrix, f as BindGroup, g as getTestContext, h as GlProgram, i as canUseNewCanvasBlendModes, it as ExtensionType, j as multiplyColors, k as getLocalBounds, l as State, m as GpuProgram, n as CanvasPool, nt as eventemitter3_default, o as Buffer, p as UniformGroup, q as nextPow2, r as canvasUtils, rt as ObservablePoint, s as BufferUsage, t as getPo2TextureFromSource, tt as Point, u as Shader, v as Ticker, w as ViewContainer, x as CanvasSource, z as Color } from "./board-kernel/getPo2TextureFromSource.js";
|
|
2
2
|
import { Worker } from "node:worker_threads";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/util.js
|
|
@@ -12305,13 +12305,13 @@ const placeHolderBufferData = /* @__PURE__ */ new Float32Array(1);
|
|
|
12305
12305
|
const placeHolderIndexData = /* @__PURE__ */ new Uint32Array(1);
|
|
12306
12306
|
var BatchGeometry = class extends Geometry {
|
|
12307
12307
|
constructor() {
|
|
12308
|
-
const attributeBuffer = new Buffer
|
|
12308
|
+
const attributeBuffer = new Buffer({
|
|
12309
12309
|
data: placeHolderBufferData,
|
|
12310
12310
|
label: "attribute-batch-buffer",
|
|
12311
12311
|
usage: BufferUsage.VERTEX | BufferUsage.COPY_DST,
|
|
12312
12312
|
shrinkToFit: false
|
|
12313
12313
|
});
|
|
12314
|
-
const indexBuffer = new Buffer
|
|
12314
|
+
const indexBuffer = new Buffer({
|
|
12315
12315
|
data: placeHolderIndexData,
|
|
12316
12316
|
label: "index-batch-buffer",
|
|
12317
12317
|
usage: BufferUsage.INDEX | BufferUsage.COPY_DST,
|
|
@@ -18288,19 +18288,19 @@ const _MeshGeometry = class _MeshGeometry extends Geometry {
|
|
|
18288
18288
|
3
|
|
18289
18289
|
]);
|
|
18290
18290
|
const shrinkToFit = options.shrinkBuffersToFit;
|
|
18291
|
-
const positionBuffer = new Buffer
|
|
18291
|
+
const positionBuffer = new Buffer({
|
|
18292
18292
|
data: positions,
|
|
18293
18293
|
label: "attribute-mesh-positions",
|
|
18294
18294
|
shrinkToFit,
|
|
18295
18295
|
usage: BufferUsage.VERTEX | BufferUsage.COPY_DST
|
|
18296
18296
|
});
|
|
18297
|
-
const uvBuffer = new Buffer
|
|
18297
|
+
const uvBuffer = new Buffer({
|
|
18298
18298
|
data: uvs,
|
|
18299
18299
|
label: "attribute-mesh-uvs",
|
|
18300
18300
|
shrinkToFit,
|
|
18301
18301
|
usage: BufferUsage.VERTEX | BufferUsage.COPY_DST
|
|
18302
18302
|
});
|
|
18303
|
-
const indexBuffer = new Buffer
|
|
18303
|
+
const indexBuffer = new Buffer({
|
|
18304
18304
|
data: indices,
|
|
18305
18305
|
label: "index-mesh-buffer",
|
|
18306
18306
|
shrinkToFit,
|
|
@@ -27161,12 +27161,24 @@ function measureBoardText(props) {
|
|
|
27161
27161
|
missingCharacters: characters
|
|
27162
27162
|
};
|
|
27163
27163
|
}
|
|
27164
|
-
function
|
|
27164
|
+
function inspectBoardItems(scene, ids) {
|
|
27165
|
+
return {
|
|
27166
|
+
diagnostics: inspectBoardScene(scene, 100, ids),
|
|
27167
|
+
items: scene.items.filter((item) => !ids || ids.has(item.id)).map((item) => ({
|
|
27168
|
+
id: item.id,
|
|
27169
|
+
bounds: item.frame,
|
|
27170
|
+
worldBounds: itemBounds(item.frame),
|
|
27171
|
+
opacity: scene.opacity(item.id),
|
|
27172
|
+
...item.type === "text" ? { text: measureBoardText(item.props) } : {}
|
|
27173
|
+
}))
|
|
27174
|
+
};
|
|
27175
|
+
}
|
|
27176
|
+
function inspectBoardScene(scene, limit = 100, ids) {
|
|
27165
27177
|
ensureBoardTextMeasurement();
|
|
27166
27178
|
const diagnostics = [];
|
|
27167
27179
|
const texts = scene.items.filter((item) => item.type === "text" && scene.opacity(item.id) > .01);
|
|
27168
27180
|
const push = (entry) => {
|
|
27169
|
-
if (diagnostics.length < limit) diagnostics.push(entry);
|
|
27181
|
+
if (diagnostics.length < limit && (!ids || ids.has(entry.item) || Boolean(entry.related && ids.has(entry.related)))) diagnostics.push(entry);
|
|
27170
27182
|
};
|
|
27171
27183
|
for (const item of scene.items) {
|
|
27172
27184
|
if (diagnostics.length >= limit) break;
|
|
@@ -27178,7 +27190,7 @@ function inspectBoardScene(scene, limit = 100) {
|
|
|
27178
27190
|
let glyphBudget = 256;
|
|
27179
27191
|
for (const item of texts) {
|
|
27180
27192
|
if (diagnostics.length >= limit || item.type !== "text") break;
|
|
27181
|
-
if (glyphBudget-- > 0) {
|
|
27193
|
+
if ((!ids || ids.has(item.id)) && glyphBudget-- > 0) {
|
|
27182
27194
|
const measured = measureBoardText(item.props);
|
|
27183
27195
|
if (measured.missingCharacters) push({
|
|
27184
27196
|
code: "glyph-unavailable",
|
|
@@ -27217,6 +27229,7 @@ function inspectBoardScene(scene, limit = 100) {
|
|
|
27217
27229
|
const b = sorted[j];
|
|
27218
27230
|
if (!b) continue;
|
|
27219
27231
|
if (b.rect.x >= a.rect.x + a.rect.width) break;
|
|
27232
|
+
if (ids && !ids.has(a.item.id) && !ids.has(b.item.id)) continue;
|
|
27220
27233
|
if (++comparisons > 1e4) return diagnostics;
|
|
27221
27234
|
if (a.item.parent === b.item.parent && rectsIntersect(a.rect, b.rect)) push({
|
|
27222
27235
|
code: "text-overlap",
|
|
@@ -31338,13 +31351,24 @@ function renderBoardExport(renderer, input, options = {}) {
|
|
|
31338
31351
|
backgroundImage: options.backgroundImage
|
|
31339
31352
|
});
|
|
31340
31353
|
try {
|
|
31341
|
-
const
|
|
31354
|
+
const texture = renderer.textureGenerator.generateTexture({
|
|
31342
31355
|
target: exportScene.root,
|
|
31343
31356
|
frame: new Rectangle(0, 0, plan.width, plan.height),
|
|
31344
31357
|
resolution: 1,
|
|
31345
31358
|
antialias: true,
|
|
31346
31359
|
clearColor: options.background === "transparent" ? void 0 : palette.bg
|
|
31347
31360
|
});
|
|
31361
|
+
let canvas;
|
|
31362
|
+
const surface = renderer.type === RendererType.CANVAS ? texture.source.resource : void 0;
|
|
31363
|
+
try {
|
|
31364
|
+
canvas = renderer.texture.generateCanvas(texture);
|
|
31365
|
+
} finally {
|
|
31366
|
+
texture.destroy(true);
|
|
31367
|
+
if (surface) {
|
|
31368
|
+
surface.width = 1;
|
|
31369
|
+
surface.height = 1;
|
|
31370
|
+
}
|
|
31371
|
+
}
|
|
31348
31372
|
const warnings = [];
|
|
31349
31373
|
if (plan.clamped) warnings.push({
|
|
31350
31374
|
kind: "scale-clamped",
|
|
@@ -31369,11 +31393,55 @@ function renderBoardExport(renderer, input, options = {}) {
|
|
|
31369
31393
|
}
|
|
31370
31394
|
}
|
|
31371
31395
|
//#endregion
|
|
31396
|
+
//#region ../sdk/src/board/export/node/fonts.ts
|
|
31397
|
+
function initializeBoardFonts(registry, fonts) {
|
|
31398
|
+
for (const font of fonts) registry.registerFromPath(font.path, font.family);
|
|
31399
|
+
if (!registry.setAlias) return;
|
|
31400
|
+
for (const [alias, candidates] of Object.entries({
|
|
31401
|
+
"system-ui": [
|
|
31402
|
+
"Noto Sans",
|
|
31403
|
+
"DejaVu Sans",
|
|
31404
|
+
"Liberation Sans",
|
|
31405
|
+
"FreeSans",
|
|
31406
|
+
"Helvetica Neue",
|
|
31407
|
+
"Arial",
|
|
31408
|
+
"Segoe UI"
|
|
31409
|
+
],
|
|
31410
|
+
"sans-serif": [
|
|
31411
|
+
"Geist",
|
|
31412
|
+
"Noto Sans",
|
|
31413
|
+
"DejaVu Sans",
|
|
31414
|
+
"Liberation Sans",
|
|
31415
|
+
"FreeSans",
|
|
31416
|
+
"Arial"
|
|
31417
|
+
],
|
|
31418
|
+
serif: [
|
|
31419
|
+
"Noto Serif",
|
|
31420
|
+
"DejaVu Serif",
|
|
31421
|
+
"Liberation Serif",
|
|
31422
|
+
"FreeSerif",
|
|
31423
|
+
"Times New Roman"
|
|
31424
|
+
],
|
|
31425
|
+
monospace: [
|
|
31426
|
+
"Geist Mono",
|
|
31427
|
+
"DejaVu Sans Mono",
|
|
31428
|
+
"Liberation Mono",
|
|
31429
|
+
"FreeMono",
|
|
31430
|
+
"Courier New"
|
|
31431
|
+
]
|
|
31432
|
+
})) {
|
|
31433
|
+
if (registry.has(alias)) continue;
|
|
31434
|
+
const family = candidates.find((name) => registry.has(name));
|
|
31435
|
+
if (family) registry.setAlias(family, alias);
|
|
31436
|
+
}
|
|
31437
|
+
}
|
|
31438
|
+
//#endregion
|
|
31372
31439
|
//#region ../sdk/src/board/export/node/scripts.ts
|
|
31373
31440
|
const WORKER = `
|
|
31374
31441
|
const {parentPort, workerData} = require('node:worker_threads');
|
|
31375
31442
|
const {createContext, Script, SourceTextModule} = require('node:vm');
|
|
31376
|
-
const {createCanvas} = require(workerData.canvasModule);
|
|
31443
|
+
const {createCanvas, GlobalFonts} = require(workerData.canvasModule);
|
|
31444
|
+
(${initializeBoardFonts.toString()})(GlobalFonts, workerData.fonts);
|
|
31377
31445
|
const instances = new Map();
|
|
31378
31446
|
const graphs = new Map();
|
|
31379
31447
|
const update = new Script('__runtime.update(JSON.parse(__inputJson)); __runtime.step(__time)');
|
|
@@ -31382,7 +31450,10 @@ const output = new Script('JSON.stringify(__runtime.output())');
|
|
|
31382
31450
|
const dispose = new Script('__runtime.destroy()');
|
|
31383
31451
|
function release(id) {
|
|
31384
31452
|
const entry=instances.get(id);instances.delete(id);
|
|
31385
|
-
if(entry) {
|
|
31453
|
+
if(entry) {
|
|
31454
|
+
try {dispose.runInContext(entry.context,{timeout:workerData.timeout});} catch {}
|
|
31455
|
+
if(entry.canvas) {entry.canvas.width=1;entry.canvas.height=1;}
|
|
31456
|
+
}
|
|
31386
31457
|
}
|
|
31387
31458
|
async function create(data) {
|
|
31388
31459
|
const context = createContext({console:{log(){},warn(){},error(){}},structuredClone},{codeGeneration:{strings:false,wasm:false}});
|
|
@@ -31432,22 +31503,29 @@ parentPort.on('message',async data=>{
|
|
|
31432
31503
|
const width=Math.max(1,Math.round(data.input.width*data.scale));
|
|
31433
31504
|
const height=Math.max(1,Math.round(data.input.height*data.scale));
|
|
31434
31505
|
const canvas=entry.canvas??=createCanvas(width,height);
|
|
31435
|
-
canvas.width=width;canvas.height=height;
|
|
31436
|
-
const drawing=canvas.getContext('2d');drawing.scale(data.scale,data.scale);
|
|
31437
|
-
context.__drawing=drawing;
|
|
31438
|
-
|
|
31506
|
+
if(canvas.width!==width || canvas.height!==height) {canvas.width=width;canvas.height=height;}
|
|
31507
|
+
const drawing=canvas.getContext('2d');drawing.reset();drawing.scale(data.scale,data.scale);
|
|
31508
|
+
context.__drawing=drawing;
|
|
31509
|
+
try {
|
|
31510
|
+
draw.runInContext(context,{timeout:workerData.timeout});
|
|
31511
|
+
png=canvas.toBuffer('image/png');
|
|
31512
|
+
} finally {
|
|
31513
|
+
delete context.__drawing;canvas.width=1;canvas.height=1;
|
|
31514
|
+
}
|
|
31439
31515
|
}
|
|
31440
31516
|
parentPort.postMessage({png,output:JSON.parse(output.runInContext(context,{timeout:workerData.timeout}))});
|
|
31441
|
-
} catch(error) {
|
|
31517
|
+
} catch(error) { release(data.input?.id);parentPort.postMessage({error:String(error.stack||error)}); }
|
|
31442
31518
|
});
|
|
31443
31519
|
`;
|
|
31444
31520
|
function createNodeBoardScriptHost(renderer, options) {
|
|
31521
|
+
const concurrency = options.concurrency ?? 4;
|
|
31522
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > 4) throw new Error("Script concurrency must be an integer between 1 and 4.");
|
|
31445
31523
|
const textures = /* @__PURE__ */ new Map();
|
|
31446
31524
|
const outputs = /* @__PURE__ */ new Map();
|
|
31447
31525
|
const errors = /* @__PURE__ */ new Map();
|
|
31448
31526
|
const sources = /* @__PURE__ */ new Map();
|
|
31449
31527
|
const modules = createBoardScriptModuleReader(options.readModule, options.readTimeoutMs);
|
|
31450
|
-
const lanes = Array.from({ length:
|
|
31528
|
+
const lanes = Array.from({ length: concurrency }, () => ({
|
|
31451
31529
|
worker: null,
|
|
31452
31530
|
tail: Promise.resolve(),
|
|
31453
31531
|
graphs: /* @__PURE__ */ new Set()
|
|
@@ -31488,6 +31566,7 @@ function createNodeBoardScriptHost(renderer, options) {
|
|
|
31488
31566
|
workerData: {
|
|
31489
31567
|
runtime: createScriptRuntime.toString(),
|
|
31490
31568
|
timeout,
|
|
31569
|
+
fonts: renderer.fonts ?? [],
|
|
31491
31570
|
canvasModule: fileURLToPath(import.meta.resolve("@napi-rs/canvas"))
|
|
31492
31571
|
}
|
|
31493
31572
|
});
|
|
@@ -31621,20 +31700,6 @@ function createNodeBoardScriptHost(renderer, options) {
|
|
|
31621
31700
|
}
|
|
31622
31701
|
//#endregion
|
|
31623
31702
|
//#region ../sdk/src/board/export/node/index.ts
|
|
31624
|
-
const SYSTEM_UI_FAMILIES = [
|
|
31625
|
-
"Noto Sans",
|
|
31626
|
-
"DejaVu Sans",
|
|
31627
|
-
"Liberation Sans",
|
|
31628
|
-
"FreeSans",
|
|
31629
|
-
"Helvetica Neue",
|
|
31630
|
-
"Arial",
|
|
31631
|
-
"Segoe UI"
|
|
31632
|
-
];
|
|
31633
|
-
function aliasSystemUi(fonts) {
|
|
31634
|
-
if (!fonts.setAlias || fonts.has("system-ui")) return;
|
|
31635
|
-
const family = SYSTEM_UI_FAMILIES.find((name) => fonts.has(name));
|
|
31636
|
-
if (family) fonts.setAlias(family, "system-ui");
|
|
31637
|
-
}
|
|
31638
31703
|
const WEBGL_PROBE_STUB = {
|
|
31639
31704
|
getShaderPrecisionFormat: () => ({
|
|
31640
31705
|
precision: 1,
|
|
@@ -31681,8 +31746,11 @@ function installAnimationFrameShim() {
|
|
|
31681
31746
|
async function createNodeBoardRenderer(options = {}) {
|
|
31682
31747
|
const canvasModule = options.canvasModule ?? await loadCanvasModule();
|
|
31683
31748
|
extensions.add(CanvasGraphicsPipe, GraphicsPipe, CanvasGraphicsContextSystem, GraphicsContextSystem, CanvasRendererTextSystem, CanvasTextSystem, CanvasTextPipe);
|
|
31684
|
-
|
|
31685
|
-
|
|
31749
|
+
const fonts = (options.fonts ?? []).map((font) => ({
|
|
31750
|
+
...font,
|
|
31751
|
+
family: font.family ?? "Geist"
|
|
31752
|
+
}));
|
|
31753
|
+
initializeBoardFonts(canvasModule.GlobalFonts, fonts);
|
|
31686
31754
|
function createCanvas(width = 1, height = 1) {
|
|
31687
31755
|
const canvas = canvasModule.createCanvas(Math.max(1, Math.ceil(width)), Math.max(1, Math.ceil(height)));
|
|
31688
31756
|
const nativeGetContext = canvas.getContext.bind(canvas);
|
|
@@ -31719,14 +31787,16 @@ async function createNodeBoardRenderer(options = {}) {
|
|
|
31719
31787
|
return {
|
|
31720
31788
|
renderer,
|
|
31721
31789
|
canvasModule,
|
|
31722
|
-
|
|
31790
|
+
fonts,
|
|
31791
|
+
decodeImage: async (bytes) => {
|
|
31723
31792
|
const image = new canvasModule.Image();
|
|
31724
|
-
const base64 = Buffer.from(bytes).toString("base64");
|
|
31725
31793
|
await new Promise((resolve, reject) => {
|
|
31726
31794
|
image.onload = () => resolve();
|
|
31727
31795
|
image.onerror = (error) => reject(error);
|
|
31728
|
-
image.src =
|
|
31796
|
+
image.src = bytes;
|
|
31729
31797
|
});
|
|
31798
|
+
image.onload = void 0;
|
|
31799
|
+
image.onerror = void 0;
|
|
31730
31800
|
return new Texture({ source: new ImageSource({
|
|
31731
31801
|
resource: image,
|
|
31732
31802
|
width: image.width,
|
|
@@ -31760,7 +31830,7 @@ function exportBoardImageBytes(renderer, document, options = {}) {
|
|
|
31760
31830
|
try {
|
|
31761
31831
|
const bytes = format === "png" ? canvas.toBuffer(MIME.png) : canvas.toBuffer(MIME[format], Math.round(quality * 100));
|
|
31762
31832
|
return {
|
|
31763
|
-
bytes: new Uint8Array(bytes),
|
|
31833
|
+
bytes: new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength),
|
|
31764
31834
|
plan: result.plan,
|
|
31765
31835
|
warnings: result.warnings,
|
|
31766
31836
|
format
|
|
@@ -31771,4 +31841,4 @@ function exportBoardImageBytes(renderer, document, options = {}) {
|
|
|
31771
31841
|
}
|
|
31772
31842
|
}
|
|
31773
31843
|
//#endregion
|
|
31774
|
-
export { BOARD_SCRIPT_DRAWING_EXAMPLE, boardDocumentAt, boardImageKeySource, boardPathTrack, boardSequence, boardSequencePatch, buildBoardScene, createBoardAnimationSampler, createBoardRenderSampler, createNodeBoardRenderer, createNodeBoardScriptHost, exportBoardImageBytes, hasBoardScripts, imageAssetKey, inspectBoardScene, measureBoardText, parseBoardDocument, planBoardExport, prepareBoardExport, selectBoardExportAssets };
|
|
31844
|
+
export { BOARD_SCRIPT_DRAWING_EXAMPLE, boardDocumentAt, boardImageKeySource, boardPathTrack, boardSequence, boardSequencePatch, buildBoardScene, createBoardAnimationSampler, createBoardRenderSampler, createNodeBoardRenderer, createNodeBoardScriptHost, exportBoardImageBytes, hasBoardScripts, imageAssetKey, inspectBoardItems, inspectBoardScene, measureBoardText, parseBoardDocument, planBoardExport, prepareBoardExport, selectBoardExportAssets };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readBoardJsonObject } from "../../board-command-support.js";
|
|
2
2
|
import { loadBoardDocument, resolveBundledFonts } from "../../board-export.js";
|
|
3
|
-
import { boardPathTrack, boardSequencePatch, buildBoardScene, createBoardAnimationSampler, createNodeBoardRenderer,
|
|
3
|
+
import { boardPathTrack, boardSequencePatch, buildBoardScene, createBoardAnimationSampler, createNodeBoardRenderer, inspectBoardItems, } from "../../board-kernel.js";
|
|
4
4
|
import { parseBoardTimes } from "../../board-time.js";
|
|
5
5
|
import { handleHttp, json as outJson } from "../../output.js";
|
|
6
6
|
import { resolveSpace } from "../../space.js";
|
|
@@ -9,9 +9,14 @@ export function registerBoardAuthoringCommands(boards) {
|
|
|
9
9
|
.command("inspect <board>")
|
|
10
10
|
.description("Measure items and report visible layout hints without changing the Board")
|
|
11
11
|
.option("--animation <id>", "Animation to sample")
|
|
12
|
-
.option("--at <times>", "Time, comma-separated times, or start:end:step", "0")
|
|
12
|
+
.option("--at <times>", "Time, comma-separated times, or start:end:step (up to 100 samples)", "0")
|
|
13
13
|
.option("--items <ids>", "Limit measurements to comma-separated item ids")
|
|
14
14
|
.option("--json", "Output as JSON")
|
|
15
|
+
.addHelpText("after", `
|
|
16
|
+
Without --animation, inspects the static document, not playback.
|
|
17
|
+
Bounds use world units with x/y at the top left; worldBounds includes rotation.
|
|
18
|
+
Checks native layout only, not camera cropping or content drawn inside scripts.
|
|
19
|
+
Example: cohub boards inspect lesson.board --animation lecture --at 0:20s:1s --json`)
|
|
15
20
|
.action(async (target, options) => {
|
|
16
21
|
try {
|
|
17
22
|
const { document } = await loadBoardDocument(await resolveSpace(boards), target);
|
|
@@ -21,20 +26,13 @@ export function registerBoardAuthoringCommands(boards) {
|
|
|
21
26
|
try {
|
|
22
27
|
const ids = options.items ? new Set(options.items.split(",")) : null;
|
|
23
28
|
outJson({
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.map((item) => ({
|
|
32
|
-
id: item.id,
|
|
33
|
-
bounds: item.frame,
|
|
34
|
-
...(item.type === "text" ? { text: measureBoardText(item.props) } : {}),
|
|
35
|
-
})),
|
|
36
|
-
};
|
|
37
|
-
}),
|
|
29
|
+
mode: sampler ? "animation" : "static",
|
|
30
|
+
animation: options.animation ?? null,
|
|
31
|
+
coverage: "Native layout in world coordinates; excludes camera cropping and script-drawn content.",
|
|
32
|
+
samples: times.map((time) => ({
|
|
33
|
+
time,
|
|
34
|
+
...inspectBoardItems(sampler?.at(time).scene ?? buildBoardScene(document), ids ?? undefined),
|
|
35
|
+
})),
|
|
38
36
|
});
|
|
39
37
|
}
|
|
40
38
|
finally {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { parseBoardJsonObject } from "../board-command-support.js";
|
|
3
3
|
export declare const parseJsonObject: typeof parseBoardJsonObject;
|
|
4
|
+
export declare function boardSchemaField(schema: Record<string, unknown> | undefined, path: string): unknown;
|
|
4
5
|
export declare function parseRect(value: string, name?: string): {
|
|
5
6
|
x: number;
|
|
6
7
|
y: number;
|