route-graphics 1.44.1 → 1.44.2

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.
@@ -745,13 +745,17 @@ var captureVideoWebm = async ({
745
745
  return;
746
746
  }
747
747
  const waiterIndex = pendingRenderWaiters.findIndex(
748
- (waiter2) => payload?.aborted !== true && (waiter2.stateId === payload?.id || waiter2.stateId === void 0)
748
+ (waiter2) => (payload?.failed === true || payload?.aborted !== true) && (waiter2.stateId === payload?.id || waiter2.stateId === void 0)
749
749
  );
750
750
  if (waiterIndex < 0) {
751
751
  return;
752
752
  }
753
753
  const [waiter] = pendingRenderWaiters.splice(waiterIndex, 1);
754
- waiter.resolve(payload);
754
+ if (payload?.failed) {
755
+ waiter.reject(payload.error ?? new Error("Render failed."));
756
+ } else {
757
+ waiter.resolve(payload);
758
+ }
755
759
  },
756
760
  debug: false
757
761
  });
@@ -798,6 +802,8 @@ var captureVideoWebm = async ({
798
802
  const stateIndex = renderPayload.stateIndexes[renderIndex];
799
803
  const state = renderPayload.states[stateIndex];
800
804
  const waitForComplete = createRenderCompleteWaiter(state.id);
805
+ void waitForComplete.catch(() => {
806
+ });
801
807
  debug("render state start", state.id);
802
808
  app.render(state);
803
809
  app.render(state);
@@ -6651,6 +6657,7 @@ Options:
6651
6657
  PNG options:
6652
6658
  --state <index> State index when YAML contains multiple states
6653
6659
  --time <ms> Sample animations at a manual time
6660
+ --layout-report <path> Write a JSON layout snapshot beside the PNG
6654
6661
  --wait-for-render-complete Wait for renderComplete before capture
6655
6662
 
6656
6663
  MP4 options:
@@ -6673,7 +6680,12 @@ Timeline inspection options:
6673
6680
  --compact Emit compact JSON instead of indented JSON
6674
6681
  `;
6675
6682
  var renderCommandUsage = usage;
6676
- var PNG_ONLY_OPTIONS = ["stateIndex", "timeMS", "waitForRenderComplete"];
6683
+ var PNG_ONLY_OPTIONS = [
6684
+ "stateIndex",
6685
+ "timeMS",
6686
+ "waitForRenderComplete",
6687
+ "layoutReportPath"
6688
+ ];
6677
6689
  var MP4_ONLY_OPTIONS = [
6678
6690
  "stateSelection",
6679
6691
  "fps",
@@ -6686,6 +6698,7 @@ var MP4_ONLY_OPTIONS = [
6686
6698
  var toOptionName = (propertyName) => ({
6687
6699
  stateIndex: "--state",
6688
6700
  timeMS: "--time",
6701
+ layoutReportPath: "--layout-report",
6689
6702
  waitForRenderComplete: "--wait-for-render-complete",
6690
6703
  stateSelection: "--states",
6691
6704
  fps: "--fps",
@@ -6870,6 +6883,10 @@ var parseRouteGraphicsCliArgs = (argv) => {
6870
6883
  options.stateSelection = readOptionValue(rest, index, token);
6871
6884
  index += 1;
6872
6885
  break;
6886
+ case "--layout-report":
6887
+ options.layoutReportPath = readOptionValue(rest, index, token);
6888
+ index += 1;
6889
+ break;
6873
6890
  case "--time":
6874
6891
  options.timeMS = parseIntegerOption(
6875
6892
  "Animation time",
@@ -7141,7 +7158,8 @@ var capturePng = async ({
7141
7158
  timeMS,
7142
7159
  waitForRenderComplete,
7143
7160
  timeoutMS,
7144
- browserExecutablePath
7161
+ browserExecutablePath,
7162
+ includeLayoutReport = false
7145
7163
  }) => {
7146
7164
  const browser = await chromium2.launch(
7147
7165
  getRendererBrowserLaunchOptions(browserExecutablePath)
@@ -7160,7 +7178,7 @@ var capturePng = async ({
7160
7178
  await page.goto(origin, {
7161
7179
  waitUntil: "domcontentloaded"
7162
7180
  });
7163
- const base64 = await page.evaluate(
7181
+ const capture = await page.evaluate(
7164
7182
  async ({ moduleUrl, renderPayload }) => {
7165
7183
  const nextFrame = async (count = 2) => {
7166
7184
  await new Promise((resolve) => {
@@ -7197,16 +7215,22 @@ var capturePng = async ({
7197
7215
  const assetBufferManager = createAssetBufferManager();
7198
7216
  let renderCompleteResolve = () => {
7199
7217
  };
7218
+ let renderCompleteReject = () => {
7219
+ };
7220
+ let renderFailure;
7200
7221
  let renderTimeoutId = null;
7201
7222
  let renderCompletePromise = Promise.resolve(null);
7202
7223
  if (renderPayload.waitForRenderComplete) {
7203
7224
  renderCompletePromise = new Promise((resolve, reject) => {
7204
7225
  renderCompleteResolve = resolve;
7226
+ renderCompleteReject = reject;
7205
7227
  renderTimeoutId = window.setTimeout(() => {
7206
7228
  reject(new Error("Timed out waiting for renderComplete."));
7207
7229
  }, renderPayload.timeoutMS);
7208
7230
  });
7209
7231
  }
7232
+ void renderCompletePromise.catch(() => {
7233
+ });
7210
7234
  try {
7211
7235
  await app.init({
7212
7236
  width: renderPayload.width,
@@ -7230,6 +7254,10 @@ var capturePng = async ({
7230
7254
  audio: [soundPlugin].filter(Boolean)
7231
7255
  },
7232
7256
  eventHandler: (eventName, payload) => {
7257
+ if (eventName === "renderComplete" && payload?.failed) {
7258
+ renderFailure = payload.error ?? new Error("Render failed.");
7259
+ renderCompleteReject(renderFailure);
7260
+ }
7233
7261
  if (eventName === "renderComplete" && payload?.aborted !== true) {
7234
7262
  renderCompleteResolve(payload);
7235
7263
  }
@@ -7254,7 +7282,10 @@ var capturePng = async ({
7254
7282
  } catch {
7255
7283
  }
7256
7284
  await nextFrame(2);
7257
- return await app.extractBase64();
7285
+ if (renderFailure) throw renderFailure;
7286
+ const image = app.extractBase64();
7287
+ const layoutReport = renderPayload.includeLayoutReport ? app.getLayoutReport() : null;
7288
+ return { base64: await image, layoutReport };
7258
7289
  } finally {
7259
7290
  if (renderTimeoutId !== null) {
7260
7291
  window.clearTimeout(renderTimeoutId);
@@ -7272,14 +7303,15 @@ var capturePng = async ({
7272
7303
  assets: browserAssets,
7273
7304
  timeMS: timeMS ?? null,
7274
7305
  waitForRenderComplete,
7275
- timeoutMS
7306
+ timeoutMS,
7307
+ includeLayoutReport
7276
7308
  }
7277
7309
  }
7278
7310
  );
7279
7311
  if (pageErrors.length > 0) {
7280
7312
  throw new Error(pageErrors.join("\n"));
7281
7313
  }
7282
- return base64;
7314
+ return capture;
7283
7315
  } finally {
7284
7316
  await browser.close();
7285
7317
  }
@@ -7301,7 +7333,7 @@ var renderPng = async ({
7301
7333
  }
7302
7334
  const settings = normalizeRenderSettings({ cliOptions, definition });
7303
7335
  const renderStartedAt = performance2.now();
7304
- const base64 = await capturePng({
7336
+ const { base64, layoutReport } = await capturePng({
7305
7337
  origin,
7306
7338
  width: settings.width,
7307
7339
  height: settings.height,
@@ -7311,11 +7343,22 @@ var renderPng = async ({
7311
7343
  timeMS: cliOptions.timeMS,
7312
7344
  waitForRenderComplete: cliOptions.waitForRenderComplete,
7313
7345
  timeoutMS: cliOptions.timeoutMS,
7314
- browserExecutablePath: cliOptions.browserExecutablePath
7346
+ browserExecutablePath: cliOptions.browserExecutablePath,
7347
+ includeLayoutReport: Boolean(cliOptions.layoutReportPath)
7315
7348
  });
7316
7349
  const renderDurationMS = performance2.now() - renderStartedAt;
7317
7350
  const writeStartedAt = performance2.now();
7318
7351
  await writePngOutput(outputPath, base64);
7352
+ if (cliOptions.layoutReportPath) {
7353
+ await fsPromises2.mkdir(path4.dirname(cliOptions.layoutReportPath), {
7354
+ recursive: true
7355
+ });
7356
+ await fsPromises2.writeFile(
7357
+ cliOptions.layoutReportPath,
7358
+ `${JSON.stringify(layoutReport, null, 2)}
7359
+ `
7360
+ );
7361
+ }
7319
7362
  const writeDurationMS = performance2.now() - writeStartedAt;
7320
7363
  return {
7321
7364
  inputPath,
@@ -7342,6 +7385,17 @@ var runRender = async ({ cliOptions, cwd }) => {
7342
7385
  outputPath: cliOptions.outputPath,
7343
7386
  format
7344
7387
  });
7388
+ if (cliOptions.layoutReportPath) {
7389
+ cliOptions = {
7390
+ ...cliOptions,
7391
+ layoutReportPath: path4.resolve(cwd, cliOptions.layoutReportPath)
7392
+ };
7393
+ if (cliOptions.layoutReportPath === inputPath || cliOptions.layoutReportPath === outputPath) {
7394
+ throw new Error(
7395
+ "Layout report path must differ from the input and PNG output paths."
7396
+ );
7397
+ }
7398
+ }
7345
7399
  await ensureBundleExists();
7346
7400
  const yamlSource = await fsPromises2.readFile(inputPath, "utf8");
7347
7401
  const definition = loadRenderDefinition(yamlSource);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "route-graphics",
3
- "version": "1.44.1",
3
+ "version": "1.44.2",
4
4
  "description": "A 2D graphics rendering interface that takes JSON input and renders pixels using PixiJS",
5
5
  "main": "dist/RouteGraphics.js",
6
6
  "type": "module",
@@ -27,12 +27,11 @@
27
27
  "files": [
28
28
  "bin",
29
29
  "dist",
30
- "src/cli",
31
30
  "package.json"
32
31
  ],
33
32
  "scripts": {
34
- "lint": "bunx prettier --check src",
35
- "lint:fix": "bunx prettier --write src",
33
+ "lint": "prettier --check src && oxlint src spec scripts bin",
34
+ "lint:fix": "prettier --write src && oxlint --fix src spec scripts bin",
36
35
  "prepare": "husky",
37
36
  "build": "node ./esbuild.js",
38
37
  "benchmark:timeline": "node ./scripts/benchmarkTimeline.mjs",
@@ -41,30 +40,37 @@
41
40
  "avt:report": "bun run build && node ./scripts/audioVisualTest.mjs report",
42
41
  "render:png": "bun run build && node ./bin/route-graphics-render.js",
43
42
  "vt:docker:pull": "docker pull docker.io/han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.1.0",
44
- "vt:generate": "bun run build && docker run --rm --user $(id -u):$(id -g) -e RTGL_VT_DEBUG=true -v \"$PWD:/workspace\" docker.io/han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.1.0 rtgl vt generate --wait-event vt:ready",
45
- "vt:screenshot": "bun run build && docker run --rm --user $(id -u):$(id -g) -e RTGL_VT_DEBUG=true -e RTGL_VT_CONCURRENCY=${RTGL_VT_CONCURRENCY:-8} -v \"$PWD:/workspace\" docker.io/han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.1.0 rtgl vt screenshot --wait-event vt:ready --concurrency ${RTGL_VT_CONCURRENCY:-8}",
43
+ "vt:generate": "bun run build:visual && docker run --rm --user $(id -u):$(id -g) -e RTGL_VT_DEBUG=true -v \"$PWD:/workspace\" docker.io/han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.1.0 rtgl vt generate --wait-event vt:ready",
44
+ "vt:screenshot": "bun run build:visual && bun run vt:screenshot:built",
46
45
  "vt:docker": "bun run vt:screenshot",
47
- "vt:report": "bun run vt:screenshot && docker run --rm --user $(id -u):$(id -g) -e RTGL_VT_DEBUG=true -v \"$PWD:/workspace\" docker.io/han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.1.0 rtgl vt report",
46
+ "vt:report": "bun run build:visual && bun run vt:report:built",
48
47
  "vt:accept": "docker run --rm --user $(id -u):$(id -g) -e RTGL_VT_DEBUG=true -v \"$PWD:/workspace\" docker.io/han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.1.0 rtgl vt accept",
49
48
  "test": "vitest run",
50
49
  "test:layout-report": "node esbuild.js && node scripts/testLayoutReport.mjs",
51
50
  "test:webgpu": "bun run build && node ./scripts/testWebGPUShaders.mjs",
52
51
  "test:coverage": "vitest run --coverage",
53
52
  "test:watch": "vitest",
54
- "serve": "bunx serve -p 3005 .rettangoli/vt/_site"
53
+ "serve": "bunx serve -p 3005 .rettangoli/vt/_site",
54
+ "build:playground": "node ./esbuild.js playground",
55
+ "build:visual": "node ./esbuild.js visual",
56
+ "build:all": "node ./esbuild.js all",
57
+ "test:runtime": "bun run build && node scripts/testRuntimeLifecycle.mjs",
58
+ "test:package": "node scripts/testPackage.mjs",
59
+ "vt:screenshot:built": "docker run --rm --user $(id -u):$(id -g) -e RTGL_VT_DEBUG=true -e RTGL_VT_CONCURRENCY=${RTGL_VT_CONCURRENCY:-8} -v \"$PWD:/workspace\" docker.io/han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.1.0 rtgl vt screenshot --wait-event vt:ready --concurrency ${RTGL_VT_CONCURRENCY:-8}",
60
+ "vt:report:built": "bun run vt:screenshot:built && docker run --rm --user $(id -u):$(id -g) -e RTGL_VT_DEBUG=true -v \"$PWD:/workspace\" docker.io/han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.1.0 rtgl vt report"
55
61
  },
56
62
  "dependencies": {
57
- "@pixi/unsafe-eval": "^7.4.3",
58
63
  "@wasm-audio-decoders/ogg-vorbis": "^0.1.20",
59
64
  "gsap": "3.15.0",
60
65
  "hotkeys-js": "^4.0.0-beta.7",
61
66
  "js-yaml": "^4.1.0",
62
67
  "ogg-opus-decoder": "^1.7.3",
63
- "pixi.js": "8.9.2",
68
+ "pixi.js": "8.10.2",
64
69
  "playwright": "1.57.0"
65
70
  },
66
71
  "devDependencies": {
67
72
  "@vitest/coverage-v8": "^4.0.8",
73
+ "canvas": "3.2.0",
68
74
  "esbuild": "^0.25.6",
69
75
  "husky": "^9.1.7",
70
76
  "jsdom": "^27.0.1",
@@ -72,6 +78,7 @@
72
78
  "oxlint": "^1.26.0",
73
79
  "pixelmatch": "^5.3.0",
74
80
  "pngjs": "^7.0.0",
81
+ "prettier": "3.6.2",
75
82
  "puty": "^0.1.2",
76
83
  "vitest": "^4.0.8"
77
84
  },
@@ -1,5 +0,0 @@
1
- export const getRendererBrowserLaunchOptions = (browserExecutablePath) => ({
2
- headless: true,
3
- executablePath: browserExecutablePath,
4
- args: ["--autoplay-policy=no-user-gesture-required"],
5
- });
@@ -1,72 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
-
4
- import { loadRenderDefinition } from "./renderConfig.js";
5
- import { normalizeRenderState } from "../util/normalizeRenderState.js";
6
- import {
7
- compileAnimationTimelineProgram,
8
- inspectTimelineProgram,
9
- } from "../plugins/animations/timeline/inspectProgram.js";
10
-
11
- export const inspectTimelineDefinition = ({
12
- definition,
13
- stateIndex = 0,
14
- animationId,
15
- }) => {
16
- if (!Number.isSafeInteger(stateIndex) || stateIndex < 0) {
17
- throw new Error(
18
- "Timeline inspection state index must be a non-negative safe integer.",
19
- );
20
- }
21
- const rawState = definition.states[stateIndex];
22
- if (!rawState) {
23
- throw new Error(
24
- `State index ${stateIndex} is out of range for ${definition.states.length} states.`,
25
- );
26
- }
27
- const state = normalizeRenderState(rawState);
28
- const indexedAnimations = state.animations.map((animation, index) => ({
29
- animation,
30
- index,
31
- }));
32
- const selected = animationId
33
- ? indexedAnimations.filter(({ animation }) => animation.id === animationId)
34
- : indexedAnimations;
35
- if (animationId && selected.length === 0) {
36
- throw new Error(
37
- `Animation "${animationId}" was not found in state "${state.id ?? stateIndex}".`,
38
- );
39
- }
40
-
41
- return {
42
- schema: "route.timeline-inspection-set/v1",
43
- state: { index: stateIndex, id: state.id ?? null },
44
- animations: selected.map(({ animation, index }) => {
45
- const sourcePath = `states[${stateIndex}].animations[${index}]`;
46
- const program = compileAnimationTimelineProgram(animation, {
47
- sourcePath,
48
- });
49
- return {
50
- normalizedAst: animation,
51
- ...inspectTimelineProgram(program),
52
- };
53
- }),
54
- };
55
- };
56
-
57
- export const inspectTimelineFile = async ({
58
- inputPath,
59
- cwd = process.cwd(),
60
- stateIndex = 0,
61
- animationId,
62
- }) => {
63
- if (!inputPath)
64
- throw new Error("Timeline inspection requires an input YAML file.");
65
- const absolutePath = path.resolve(cwd, inputPath);
66
- const yamlSource = await fs.readFile(absolutePath, "utf8");
67
- return inspectTimelineDefinition({
68
- definition: loadRenderDefinition(yamlSource),
69
- stateIndex,
70
- animationId,
71
- });
72
- };
@@ -1,135 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import fs from "node:fs";
3
- import { inspectTimelineDefinition } from "./inspectTimeline.js";
4
- import { loadRenderDefinition } from "./renderConfig.js";
5
- import {
6
- parseRouteGraphicsCliArgs,
7
- runRouteGraphicsCli,
8
- } from "./routeGraphicsCli.js";
9
-
10
- const source = `
11
- states:
12
- - id: demo
13
- elements: []
14
- animations:
15
- - id: move
16
- targetId: hero
17
- type: update
18
- gsap:
19
- profile: portable-v1
20
- steps:
21
- - kind: mark
22
- name: entrance
23
- - kind: to
24
- values: { x: 100 }
25
- duration: 250
26
- `;
27
-
28
- describe("timeline inspector", () => {
29
- it("reports normalized authoring, semantic program, and visualization lanes", () => {
30
- const result = inspectTimelineDefinition({
31
- definition: loadRenderDefinition(source),
32
- stateIndex: 0,
33
- animationId: "move",
34
- });
35
- expect(result.animations[0]).toMatchObject({
36
- normalizedAst: { id: "move", gsap: { profile: "portable-v1" } },
37
- summary: { duration: 250, clipCount: 1, frontend: "gsap" },
38
- visualization: {
39
- marks: { entrance: 0 },
40
- lanes: [{ channel: "transform.x", start: 0, duration: 250 }],
41
- },
42
- });
43
- expect(result.animations[0].semanticSignature).toContain(
44
- '"schema":"route.timeline/v1"',
45
- );
46
- });
47
-
48
- it("parses and runs inspect-timeline without launching a browser", async () => {
49
- expect(
50
- parseRouteGraphicsCliArgs([
51
- "inspect-timeline",
52
- "scene.yaml",
53
- "--animation",
54
- "move",
55
- "--compact",
56
- ]),
57
- ).toMatchObject({
58
- command: "inspect-timeline",
59
- inputPath: "scene.yaml",
60
- animationId: "move",
61
- compact: true,
62
- });
63
-
64
- const stdout = {
65
- value: "",
66
- write(value) {
67
- this.value += value;
68
- },
69
- };
70
- const stderr = {
71
- value: "",
72
- write(value) {
73
- this.value += value;
74
- },
75
- };
76
- const exitCode = await runRouteGraphicsCli({
77
- argv: ["inspect-timeline", "virtual.yaml", "--compact"],
78
- cwd: "/virtual",
79
- stdout,
80
- stderr,
81
- });
82
- expect(exitCode).toBe(1);
83
- expect(stdout.value).toBe("");
84
- expect(stderr.value).toContain("ENOENT");
85
- expect(() =>
86
- parseRouteGraphicsCliArgs([
87
- "inspect-timeline",
88
- "scene.yaml",
89
- "--output",
90
- "ignored.json",
91
- ]),
92
- ).toThrow("--output is not supported by inspect-timeline");
93
- });
94
-
95
- it("preserves original animation indexes in source paths when filtering", () => {
96
- const result = inspectTimelineDefinition({
97
- definition: loadRenderDefinition(`
98
- states:
99
- - elements: []
100
- animations:
101
- - id: first
102
- targetId: one
103
- type: update
104
- tween:
105
- x:
106
- keyframes: [{ value: 1, duration: 10 }]
107
- - id: second
108
- targetId: two
109
- type: update
110
- tween:
111
- x:
112
- keyframes: [{ value: 2, duration: 10 }]
113
- `),
114
- animationId: "second",
115
- });
116
- expect(result.animations[0].visualization.lanes[0].sourcePath).toContain(
117
- "animations[1]",
118
- );
119
- });
120
-
121
- it("compiles the checked-in portable GSAP example", () => {
122
- const example = fs.readFileSync(
123
- `${process.cwd()}/examples/portable-gsap.yaml`,
124
- "utf8",
125
- );
126
- const result = inspectTimelineDefinition({
127
- definition: loadRenderDefinition(example),
128
- });
129
- expect(result.animations[0].summary).toMatchObject({
130
- programId: "card-entrance",
131
- frontend: "gsap",
132
- eventCount: 1,
133
- });
134
- });
135
- });