@tileflow/capture 0.1.0-alpha.29 → 0.1.0-alpha.31

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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # @tileflow/capture
2
2
 
3
+ For the complete CLI workflow, use `describe`, `inspect`, `capture`, `compare`, and Map DevTools (`preview`)
4
+ as described in the [CLI guide](https://github.com/tileflow/tileflow-sdk/blob/main/packages/cli/README.md).
5
+ New still captures include their normalized `definition` for later review. The CLI stores immutable
6
+ capture IDs so `compare --before <id> --after <id>` can reuse evidence without rendering again.
7
+
3
8
  Headless Tileflow map capture for Node.js. Produce PNGs with versioned receipts, compare reviewed
4
9
  baselines, or inspect differences between deliberate renders. Use the
5
10
  [CLI](https://github.com/tileflow/tileflow-sdk/blob/main/packages/cli/README.md) for ordinary terminal
@@ -106,6 +111,66 @@ The equivalent CLI command is `tileflow setup capture`. These settings control b
106
111
  not the network access required by a map's remote resources. Tileflow never deletes the shared
107
112
  per-user browser cache.
108
113
 
114
+ ## Render a timed shot
115
+
116
+ A [capture shot](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/capture-shots.md)
117
+ is a committed camera move with keyframes and an optional theme blend. This module assumes a
118
+ `dusk-flyover` shot in `tileflow.config.ts` and writes its WebM video and receipt:
119
+
120
+ <!-- docs:check -->
121
+
122
+ ```ts
123
+ import {mkdir, writeFile} from 'node:fs/promises';
124
+ import {captureTileflowShot, serializeTileflowShotReceipt} from '@tileflow/capture';
125
+
126
+ const shot = await captureTileflowShot({shot: 'dusk-flyover', allowBrowserInstall: true});
127
+ if (!shot.video) throw new Error('The shot produced no video.');
128
+
129
+ await mkdir('.tileflow/captures/shots', {recursive: true});
130
+ await writeFile('.tileflow/captures/shots/dusk-flyover.webm', shot.video);
131
+ await writeFile(
132
+ '.tileflow/captures/shots/dusk-flyover.receipt.json',
133
+ serializeTileflowShotReceipt(shot.receipt),
134
+ );
135
+ console.log(shot.frameCount, shot.codec, shot.framesSha256);
136
+ ```
137
+
138
+ Every frame moves the camera with MapLibre `jumpTo`, moves the blend position, and waits for
139
+ MapLibre `idle` and two animation frames before its screenshot, exactly as a scene does. A slow
140
+ machine or network makes the render take longer; the video keeps its frame rate and shows every
141
+ frame. `timeoutMs` bounds page setup and each frame separately.
142
+
143
+ The pinned browser encodes the captured PNGs with WebCodecs: VP9, or VP8 where VP9 is unavailable.
144
+ A browser that supports neither fails before the first frame with `VIDEO_ENCODER_UNAVAILABLE`.
145
+ Use `format: 'frames'` to receive the PNG frames through `onFrame` instead of a video; frames are
146
+ delivered in order and are not kept in memory:
147
+
148
+ <!-- docs:check -->
149
+
150
+ ```ts
151
+ import {mkdir, writeFile} from 'node:fs/promises';
152
+ import {createTileflowCaptureSession} from '@tileflow/capture';
153
+
154
+ await mkdir('frames', {recursive: true});
155
+ const session = createTileflowCaptureSession({allowBrowserInstall: true});
156
+ try {
157
+ const shot = await session.captureShot('dusk-flyover', {
158
+ format: 'frames',
159
+ onFrame: (frame) =>
160
+ writeFile(`frames/frame-${String(frame.index).padStart(4, '0')}.png`, frame.png),
161
+ });
162
+ console.log(shot.frameCount, shot.framesSha256);
163
+ } finally {
164
+ await session.close();
165
+ }
166
+ ```
167
+
168
+ `captureShots` and `captureAllShots` render several shots from one compiled snapshot. A shot
169
+ receipt records the shot, theme and blend styles, timing, dimensions, renderer, data identity, and
170
+ `frames.sha256`, a digest of every frame's PNG SHA-256. Frames are reproducible under the same
171
+ conditions as scene captures; encoder bytes may differ between runs, so `output.sha256` identifies
172
+ the written video while `frames.sha256` identifies its pixels.
173
+
109
174
  ## Capture the running application
110
175
 
111
176
  Application capture uses the app's normal loopback server; it does not start a server or discover a
@@ -183,7 +248,9 @@ the perceptual metric and optional diff image are additional evidence. Never upd
183
248
  merely to make a failing comparison pass.
184
249
 
185
250
  `compareTileflowCapturesForReview` compares two deliberate renders with different styles while
186
- requiring compatible framing, renderer, and data identity. `analyzeTileflowCaptureReference` analyzes
251
+ requiring compatible framing, renderer, and data identity. It accepts captures with or without a
252
+ retained `definition`; when present, that definition must match the explicit review definition.
253
+ `analyzeTileflowCaptureReference` analyzes
187
254
  an external PNG without claiming receipt compatibility. Neither operation approves a baseline.
188
255
  See the [receipt and comparison reference](https://github.com/tileflow/tileflow-sdk/blob/main/packages/capture/docs/receipts-and-comparison.md),
189
256
  also included under `docs/` in the installed package, for identities, metrics, and limits.
@@ -195,7 +262,8 @@ validation, browser startup, resource loading, MapLibre readiness, and screensho
195
262
  Resource diagnostics contain safe classification and origin/status information, not credentials,
196
263
  query strings, response bodies, raw browser stacks, or DOM contents.
197
264
 
198
- Receipts use schema version 4; parsing rejects any other version. Import
265
+ Receipts use schema version 4; parsing rejects any other version. Shot receipts use their own
266
+ schema version 1. Import
199
267
  `@tileflow/capture/receipt` for receipt-only tooling without loading the capture runtime. World selectors resolve once per capture session to
200
268
  an immutable release. Capture retains the complete resolved TileJSON, including its zoom range,
201
269
  bounds, attribution and vector-layer metadata. Above the data's maximum zoom, MapLibre enlarges
@@ -6,6 +6,7 @@ import {
6
6
  serializeCanonicalJson,
7
7
  tileflowCaptureSceneLimits,
8
8
  tileflowCaptureSceneNameSchema,
9
+ tileflowCaptureShotLimits,
9
10
  tileflowPortableIdSchema,
10
11
  tileflowThemeNameSchema
11
12
  } from "@tileflow/core";
@@ -231,6 +232,219 @@ function validateTileflowCaptureReceipt(value) {
231
232
  assertCanonicalReceiptByteLimit(normalized);
232
233
  return normalized;
233
234
  }
235
+ var tileflowShotReceiptSchemaVersion = 1;
236
+ var tileflowShotReceiptLimits = Object.freeze({ maximumBytes: 64 * 1024 });
237
+ function createTileflowShotReceipt(input) {
238
+ return validateTileflowShotReceipt({
239
+ schemaVersion: tileflowShotReceiptSchemaVersion,
240
+ kind: "shot",
241
+ shot: { name: input.shot, map: input.map, theme: input.theme, sha256: input.shotSha256 },
242
+ style: { sha256: input.styleSha256 },
243
+ ...input.blend ? { blend: { themes: input.blend } } : {},
244
+ timing: { fps: input.fps, frameCount: input.frameCount, durationMs: input.durationMs },
245
+ image: {
246
+ cssWidth: input.width,
247
+ cssHeight: input.height,
248
+ physicalWidth: input.width * input.dpr,
249
+ physicalHeight: input.height * input.dpr,
250
+ dpr: input.dpr
251
+ },
252
+ frames: { sha256: input.framesSha256 },
253
+ output: input.output,
254
+ renderer: input.renderer,
255
+ platform: { os: process.platform, architecture: process.arch },
256
+ data: normalizeCaptureDataInput(input.data),
257
+ verification: { data: "rendered", style: "rendered" },
258
+ networkDependent: input.networkDependent
259
+ });
260
+ }
261
+ function serializeTileflowShotReceipt(receipt) {
262
+ const serialized = `${serializeCanonicalJson(validateTileflowShotReceipt(receipt))}
263
+ `;
264
+ if (new TextEncoder().encode(serialized).byteLength > tileflowShotReceiptLimits.maximumBytes) {
265
+ throw invalidReceipt("The shot receipt exceeds the supported byte limit.");
266
+ }
267
+ return serialized;
268
+ }
269
+ function parseTileflowShotReceipt(source) {
270
+ const byteLength = typeof source === "string" ? new TextEncoder().encode(source).byteLength : source.byteLength;
271
+ if (byteLength > tileflowShotReceiptLimits.maximumBytes) {
272
+ throw invalidReceipt("The shot receipt exceeds the supported byte limit.");
273
+ }
274
+ let value;
275
+ let text;
276
+ try {
277
+ text = typeof source === "string" ? source : new TextDecoder("utf-8", { fatal: true }).decode(source);
278
+ value = JSON.parse(text);
279
+ } catch (error) {
280
+ throw invalidReceipt("The shot receipt is not valid UTF-8 JSON.", error);
281
+ }
282
+ const receipt = validateTileflowShotReceipt(value);
283
+ if (text.trim() !== serializeCanonicalJson(value)) {
284
+ throw invalidReceipt("The shot receipt is not canonical JSON.");
285
+ }
286
+ return receipt;
287
+ }
288
+ function validateTileflowShotReceipt(value) {
289
+ assertPlainReceiptData(value, "receipt", {
290
+ allowArrays: true,
291
+ ancestors: /* @__PURE__ */ new WeakSet(),
292
+ nodes: 0
293
+ });
294
+ const receipt = requireRecord(value, "receipt");
295
+ if (receipt.schemaVersion !== tileflowShotReceiptSchemaVersion || receipt.kind !== "shot") {
296
+ throw invalidReceipt("The shot receipt schema version is unsupported.");
297
+ }
298
+ requireExactKeys(receipt, [
299
+ "schemaVersion",
300
+ "kind",
301
+ "shot",
302
+ "style",
303
+ ...receipt.blend === void 0 ? [] : ["blend"],
304
+ "timing",
305
+ "image",
306
+ "frames",
307
+ "output",
308
+ "renderer",
309
+ "platform",
310
+ "data",
311
+ "verification",
312
+ "networkDependent"
313
+ ]);
314
+ const shot = requireRecord(receipt.shot, "shot");
315
+ requireExactKeys(shot, ["name", "map", "theme", "sha256"]);
316
+ const style = requireRecord(receipt.style, "style");
317
+ requireExactKeys(style, ["sha256"]);
318
+ const blend = receipt.blend === void 0 ? void 0 : validateShotBlend(receipt.blend);
319
+ const timing = requireRecord(receipt.timing, "timing");
320
+ requireExactKeys(timing, ["fps", "frameCount", "durationMs"]);
321
+ const fps = timing.fps;
322
+ if (fps !== 24 && fps !== 25 && fps !== 30 && fps !== 50 && fps !== 60) {
323
+ throw invalidReceipt("The shot receipt has an invalid timing.fps.");
324
+ }
325
+ const frameCount = requirePositiveInteger(timing.frameCount, "timing.frameCount");
326
+ const durationMs = requirePositiveInteger(timing.durationMs, "timing.durationMs");
327
+ if (durationMs > tileflowCaptureShotLimits.durationMs.maximum || frameCount > tileflowCaptureShotLimits.maximumFrames || frameCount !== Math.floor(durationMs * fps / 1e3 + 1e-9) + 1) {
328
+ throw invalidReceipt("The shot receipt timing is inconsistent.");
329
+ }
330
+ const image = requireRecord(receipt.image, "image");
331
+ requireExactKeys(image, ["cssWidth", "cssHeight", "physicalWidth", "physicalHeight", "dpr"]);
332
+ const cssWidth = requirePositiveInteger(image.cssWidth, "image.cssWidth");
333
+ const cssHeight = requirePositiveInteger(image.cssHeight, "image.cssHeight");
334
+ const physicalWidth = requirePositiveInteger(image.physicalWidth, "image.physicalWidth");
335
+ const physicalHeight = requirePositiveInteger(image.physicalHeight, "image.physicalHeight");
336
+ if (image.dpr !== 1 && image.dpr !== 2) {
337
+ throw invalidReceipt("The shot receipt has an invalid image DPR.");
338
+ }
339
+ if (cssWidth * image.dpr !== physicalWidth || cssHeight * image.dpr !== physicalHeight) {
340
+ throw invalidReceipt("The shot receipt image dimensions are inconsistent.");
341
+ }
342
+ if (physicalWidth > tileflowCaptureSceneLimits.viewport.maximum || physicalHeight > tileflowCaptureSceneLimits.viewport.maximum || physicalWidth * physicalHeight > tileflowCaptureSceneLimits.maximumPhysicalPixels) {
343
+ throw invalidReceipt("The shot receipt image exceeds the supported pixel limit.");
344
+ }
345
+ const frames = requireRecord(receipt.frames, "frames");
346
+ requireExactKeys(frames, ["sha256"]);
347
+ const framesSha256 = requireHash(frames.sha256, "frames.sha256");
348
+ const output = requireRecord(receipt.output, "output");
349
+ if (output.kind === "webm") {
350
+ requireExactKeys(output, ["kind", "codec", "sha256"]);
351
+ if (output.codec !== "vp8" && output.codec !== "vp9") {
352
+ throw invalidReceipt("The shot receipt has an invalid output.codec.");
353
+ }
354
+ } else if (output.kind === "frames") {
355
+ requireExactKeys(output, ["kind", "sha256"]);
356
+ if (output.sha256 !== framesSha256) {
357
+ throw invalidReceipt("The shot receipt frame output does not match its frames.");
358
+ }
359
+ } else {
360
+ throw invalidReceipt("The shot receipt has an invalid output.kind.");
361
+ }
362
+ const renderer = requireRecord(receipt.renderer, "renderer");
363
+ requireExactKeys(renderer, [
364
+ "tileflow",
365
+ "maplibre",
366
+ "playwright",
367
+ "chromiumRevision",
368
+ "chromiumVersion"
369
+ ]);
370
+ const platform = requireRecord(receipt.platform, "platform");
371
+ requireExactKeys(platform, ["os", "architecture"]);
372
+ const verification = requireRecord(receipt.verification, "verification");
373
+ requireExactKeys(verification, ["data", "style"]);
374
+ if (verification.data !== "rendered" || verification.style !== "rendered") {
375
+ throw invalidReceipt("The shot receipt verification must record rendered style and data.");
376
+ }
377
+ if (typeof receipt.networkDependent !== "boolean") {
378
+ throw invalidReceipt("The shot receipt has an invalid network dependency flag.");
379
+ }
380
+ const normalized = {
381
+ schemaVersion: tileflowShotReceiptSchemaVersion,
382
+ kind: "shot",
383
+ shot: {
384
+ name: requireSceneName(shot.name),
385
+ map: requirePortableIdentifier(shot.map, "shot.map"),
386
+ theme: requireShotTheme(shot.theme, "shot.theme"),
387
+ sha256: requireHash(shot.sha256, "shot.sha256")
388
+ },
389
+ style: { sha256: requireHash(style.sha256, "style.sha256") },
390
+ ...blend ? { blend } : {},
391
+ timing: { fps, frameCount, durationMs },
392
+ image: { cssWidth, cssHeight, physicalWidth, physicalHeight, dpr: image.dpr },
393
+ frames: { sha256: framesSha256 },
394
+ output: output.kind === "webm" ? {
395
+ kind: "webm",
396
+ codec: output.codec,
397
+ sha256: requireHash(output.sha256, "output.sha256")
398
+ } : { kind: "frames", sha256: framesSha256 },
399
+ renderer: {
400
+ tileflow: requireBoundedString(renderer.tileflow, "renderer.tileflow"),
401
+ maplibre: requireBoundedString(renderer.maplibre, "renderer.maplibre"),
402
+ playwright: requireBoundedString(renderer.playwright, "renderer.playwright"),
403
+ chromiumRevision: requireBoundedString(
404
+ renderer.chromiumRevision,
405
+ "renderer.chromiumRevision"
406
+ ),
407
+ chromiumVersion: requireBoundedString(renderer.chromiumVersion, "renderer.chromiumVersion")
408
+ },
409
+ platform: {
410
+ os: requireBoundedString(platform.os, "platform.os"),
411
+ architecture: requireBoundedString(platform.architecture, "platform.architecture")
412
+ },
413
+ data: validateDataIdentity(receipt.data),
414
+ verification: { data: "rendered", style: "rendered" },
415
+ networkDependent: receipt.networkDependent
416
+ };
417
+ const serialized = `${serializeCanonicalJson(normalized)}
418
+ `;
419
+ if (new TextEncoder().encode(serialized).byteLength > tileflowShotReceiptLimits.maximumBytes) {
420
+ throw invalidReceipt("The shot receipt exceeds the supported byte limit.");
421
+ }
422
+ return normalized;
423
+ }
424
+ function requireShotTheme(value, field) {
425
+ const result = tileflowThemeNameSchema.safeParse(value);
426
+ if (!result.success) throw invalidReceipt(`The shot receipt has an invalid ${field}.`);
427
+ return result.data;
428
+ }
429
+ function validateShotBlend(value) {
430
+ const blend = requireRecord(value, "blend");
431
+ requireExactKeys(blend, ["themes"]);
432
+ if (!Array.isArray(blend.themes) || blend.themes.length < tileflowCaptureShotLimits.blendThemes.minimum || blend.themes.length > tileflowCaptureShotLimits.blendThemes.maximum) {
433
+ throw invalidReceipt("The shot receipt has an invalid blend.themes list.");
434
+ }
435
+ const themes = blend.themes.map((entry, index) => {
436
+ const theme = requireRecord(entry, `blend.themes.${index}`);
437
+ requireExactKeys(theme, ["name", "styleSha256"]);
438
+ return {
439
+ name: requireShotTheme(theme.name, `blend.themes.${index}.name`),
440
+ styleSha256: requireHash(theme.styleSha256, `blend.themes.${index}.styleSha256`)
441
+ };
442
+ });
443
+ if (new Set(themes.map((theme) => theme.name)).size !== themes.length) {
444
+ throw invalidReceipt("The shot receipt blends a theme more than once.");
445
+ }
446
+ return { themes };
447
+ }
234
448
  function validateDataIdentity(value) {
235
449
  const data = requireRecord(value, "data");
236
450
  if (data.kind === "tileflow-world") {
@@ -390,12 +604,17 @@ function assertCanonicalReceiptByteLimit(receipt) {
390
604
  }
391
605
  }
392
606
  function assertPlainReceiptData(value, path = "receipt", state = {
607
+ allowArrays: false,
393
608
  ancestors: /* @__PURE__ */ new WeakSet(),
394
609
  nodes: 0
395
610
  }, depth = 0) {
396
611
  if (value === null || value === void 0 || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
397
612
  return;
398
613
  }
614
+ if (typeof value === "object" && Array.isArray(value) && state.allowArrays) {
615
+ assertPlainReceiptArray(value, path, state, depth);
616
+ return;
617
+ }
399
618
  if (typeof value !== "object" || Array.isArray(value)) {
400
619
  throw invalidReceipt(`The baseline receipt has an executable or non-plain ${path} value.`);
401
620
  }
@@ -434,6 +653,35 @@ function assertPlainReceiptData(value, path = "receipt", state = {
434
653
  state.ancestors.delete(value);
435
654
  }
436
655
  }
656
+ function assertPlainReceiptArray(value, path, state, depth) {
657
+ if (nodeUtilTypes.isProxy(value)) {
658
+ throw invalidReceipt(`The baseline receipt has an executable proxy at ${path}.`);
659
+ }
660
+ if (Object.getPrototypeOf(value) !== Array.prototype) {
661
+ throw invalidReceipt(`The baseline receipt has a non-plain ${path} array.`);
662
+ }
663
+ if (depth > 32 || ++state.nodes > 4096 || value.length > 4096) {
664
+ throw invalidReceipt("The baseline receipt exceeds the supported structural limit.");
665
+ }
666
+ if (state.ancestors.has(value)) {
667
+ throw invalidReceipt("The baseline receipt must not contain cyclic data.");
668
+ }
669
+ if (Reflect.ownKeys(value).length !== value.length + 1) {
670
+ throw invalidReceipt(`The baseline receipt has a sparse or extended ${path} array.`);
671
+ }
672
+ state.ancestors.add(value);
673
+ try {
674
+ for (let index = 0; index < value.length; index += 1) {
675
+ const descriptor = Object.getOwnPropertyDescriptor(value, index);
676
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
677
+ throw invalidReceipt("The baseline receipt contains an accessor or non-enumerable field.");
678
+ }
679
+ assertPlainReceiptData(descriptor.value, `${path}.${index}`, state, depth + 1);
680
+ }
681
+ } finally {
682
+ state.ancestors.delete(value);
683
+ }
684
+ }
437
685
  function requireRecord(value, field) {
438
686
  if (!value || typeof value !== "object" || Array.isArray(value)) {
439
687
  throw invalidReceipt(`The baseline receipt has an invalid ${field} object.`);
@@ -544,5 +792,11 @@ export {
544
792
  createTileflowCaptureReceipt,
545
793
  serializeTileflowCaptureReceipt,
546
794
  parseTileflowCaptureReceipt,
547
- validateTileflowCaptureReceipt
795
+ validateTileflowCaptureReceipt,
796
+ tileflowShotReceiptSchemaVersion,
797
+ tileflowShotReceiptLimits,
798
+ createTileflowShotReceipt,
799
+ serializeTileflowShotReceipt,
800
+ parseTileflowShotReceipt,
801
+ validateTileflowShotReceipt
548
802
  };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Browser } from 'playwright';
2
- import { T as TileflowCaptureRendererIdentity, a as TileflowCaptureReceipt, b as TileflowCaptureDataInput, c as TileflowCaptureWorldIdentity } from './receipt-DhFOATRO.js';
3
- export { C as CreateTileflowCaptureReceiptInput, d as TileflowCaptureDataBindings, e as TileflowCaptureDataCapabilities, f as TileflowCaptureDataIdentity, g as TileflowCaptureDataSemantics, h as TileflowCaptureDataSource, i as TileflowCaptureVectorIdentity, j as TileflowCaptureVerification, k as createTileflowCaptureReceipt, l as createTileflowCaptureRendererIdentity, p as parseTileflowCaptureReceipt, s as serializeTileflowCaptureReceipt, t as tileflowCaptureReceiptLimits, m as tileflowCaptureReceiptSchemaVersion, n as tileflowCaptureRuntime, v as validateTileflowCaptureReceipt } from './receipt-DhFOATRO.js';
4
- import { TileflowCaptureScene, MapLibreStyle, NormalizedTileflowCaptureScene } from '@tileflow/core';
5
- import { TileflowIconResolutionOptions, TileflowBuildArtifacts, TileflowBuildAsset } from '@tileflow/dev/artifacts';
2
+ import { T as TileflowCaptureRendererIdentity, a as TileflowShotReceiptV1, b as TileflowCaptureReceipt, c as TileflowCaptureDataInput, d as TileflowCaptureWorldIdentity } from './receipt-CfZkzyXd.js';
3
+ export { C as CreateTileflowCaptureReceiptInput, e as CreateTileflowShotReceiptInput, f as TileflowCaptureDataBindings, g as TileflowCaptureDataCapabilities, h as TileflowCaptureDataIdentity, i as TileflowCaptureDataSemantics, j as TileflowCaptureDataSource, k as TileflowCaptureVectorIdentity, l as TileflowCaptureVerification, m as createTileflowCaptureReceipt, n as createTileflowCaptureRendererIdentity, o as createTileflowShotReceipt, p as parseTileflowCaptureReceipt, q as parseTileflowShotReceipt, s as serializeTileflowCaptureReceipt, r as serializeTileflowShotReceipt, t as tileflowCaptureReceiptLimits, u as tileflowCaptureReceiptSchemaVersion, v as tileflowCaptureRuntime, w as tileflowShotReceiptLimits, x as tileflowShotReceiptSchemaVersion, y as validateTileflowCaptureReceipt, z as validateTileflowShotReceipt } from './receipt-CfZkzyXd.js';
4
+ import { NormalizedTileflowCaptureShot, NormalizedTileflowCaptureScene, TileflowCaptureScene, MapLibreStyle } from '@tileflow/core';
5
+ import { TileflowBuildAsset, TileflowIconResolutionOptions, TileflowBuildArtifacts } from '@tileflow/dev/artifacts';
6
6
 
7
7
  type TileflowBrowserInstallProgress = 'installing' | 'installed';
8
8
  type TileflowBrowserLaunchOptions = {
@@ -24,6 +24,49 @@ declare class TileflowCaptureBrowserManager {
24
24
  close(): Promise<void>;
25
25
  }
26
26
 
27
+ declare const tileflowSyntheticAssetOrigin = "https://tileflow.local.invalid";
28
+ type TileflowCaptureAsset = TileflowBuildAsset & {
29
+ url?: string;
30
+ };
31
+
32
+ type TileflowShotVideoCodec = 'vp8' | 'vp9';
33
+
34
+ type TileflowShotFormat = 'frames' | 'webm';
35
+ /** One rendered frame, delivered in order while a shot renders. */
36
+ type TileflowShotFrame = {
37
+ shot: string;
38
+ index: number;
39
+ png: Uint8Array;
40
+ sha256: string;
41
+ };
42
+ type TileflowShotCapture = {
43
+ shot: string;
44
+ map: string;
45
+ theme: string;
46
+ format: TileflowShotFormat;
47
+ /** WebM bytes for the `webm` format. Frames reach `onFrame` and are not retained. */
48
+ video?: Uint8Array;
49
+ codec?: TileflowShotVideoCodec;
50
+ /** SHA-256 of the video, or of the frame set (`framesSha256`) for the `frames` format. */
51
+ sha256: string;
52
+ framesSha256: string;
53
+ frameSha256s: string[];
54
+ shotSha256: string;
55
+ styleSha256: string;
56
+ fps: NormalizedTileflowCaptureShot['fps'];
57
+ frameCount: number;
58
+ durationMs: number;
59
+ width: number;
60
+ height: number;
61
+ dpr: 1 | 2;
62
+ networkDependent: boolean;
63
+ renderer: TileflowCaptureRendererIdentity;
64
+ receipt: TileflowShotReceiptV1;
65
+ warnings: string[];
66
+ };
67
+ /** SHA-256 of each frame's PNG SHA-256 in lowercase hex, one per line, in frame order. */
68
+ declare function tileflowShotFramesSha256(frameSha256s: readonly string[]): Promise<string>;
69
+
27
70
  declare const tileflowCaptureResultSchemaVersion: 1;
28
71
  type TileflowCaptureOptions = {
29
72
  allowBrowserInstall?: boolean;
@@ -47,6 +90,10 @@ type TileflowCaptureOptions = {
47
90
  };
48
91
  type CreateTileflowCaptureSessionOptions = Omit<TileflowCaptureOptions, 'scenes'>;
49
92
  type TileflowCapture = {
93
+ /** Concrete normalized frame retained for later review without rendering again. */
94
+ definition?: NormalizedTileflowCaptureScene & {
95
+ theme: string;
96
+ };
50
97
  scene: string;
51
98
  map: string;
52
99
  theme: string;
@@ -68,18 +115,39 @@ type TileflowCaptureResult = {
68
115
  schemaVersion: 1;
69
116
  captures: TileflowCapture[];
70
117
  };
118
+ type TileflowShotCaptureOptions = {
119
+ /** `webm` (the default) encodes a VP9 or VP8 video; `frames` delivers only PNG frames. */
120
+ format?: TileflowShotFormat;
121
+ /** Receives each frame's PNG in order while the shot renders. */
122
+ onFrame?: (frame: TileflowShotFrame) => Promise<void> | void;
123
+ signal?: AbortSignal;
124
+ };
125
+ type TileflowShotCaptureResult = {
126
+ schemaVersion: 1;
127
+ shots: TileflowShotCapture[];
128
+ };
129
+ type CaptureTileflowShotOptions = CreateTileflowCaptureSessionOptions & Omit<TileflowShotCaptureOptions, 'signal'> & {
130
+ shot: string;
131
+ };
71
132
  type TileflowCaptureSession = {
72
133
  capture(scenes: string[], signal?: AbortSignal): Promise<TileflowCaptureResult>;
73
134
  captureAll(signal?: AbortSignal): Promise<TileflowCaptureResult>;
74
135
  captureDefinitions(scenes: Record<string, TileflowCaptureScene>, signal?: AbortSignal): Promise<TileflowCaptureResult>;
75
136
  captureArtifacts(artifacts: TileflowBuildArtifacts, requestedScenes: string[], signal?: AbortSignal): Promise<TileflowCaptureResult>;
137
+ /** Renders one committed shot. */
138
+ captureShot(shot: string, options?: TileflowShotCaptureOptions): Promise<TileflowShotCapture>;
139
+ /** Renders committed shots in name order from one compiled artifact snapshot. */
140
+ captureShots(shots: string[], options?: TileflowShotCaptureOptions): Promise<TileflowShotCaptureResult>;
141
+ captureAllShots(options?: TileflowShotCaptureOptions): Promise<TileflowShotCaptureResult>;
76
142
  close(): Promise<void>;
77
143
  };
78
144
  declare function captureTileflowScenes(options: TileflowCaptureOptions): Promise<TileflowCaptureResult>;
145
+ /** Renders one committed shot with a temporary session. */
146
+ declare function captureTileflowShot(options: CaptureTileflowShotOptions): Promise<TileflowShotCapture>;
79
147
  declare function createTileflowCaptureSession(options?: CreateTileflowCaptureSessionOptions): TileflowCaptureSession;
80
148
 
81
- type TileflowCaptureErrorCode = 'ABORTED' | 'APPLICATION_ORIGIN_REQUIRED' | 'APPLICATION_ORIGIN_INVALID' | 'APPLICATION_NAVIGATION_FAILED' | 'APPLICATION_TARGET_NOT_FOUND' | 'APPLICATION_TARGET_AMBIGUOUS' | 'APPLICATION_ERROR' | 'BASELINE_INVALID' | 'BROWSER_INSTALL_FAILED' | 'BROWSER_MISSING' | 'BROWSER_START_FAILED' | 'CAPTURE_TIMEOUT' | 'INVALID_PNG' | 'MAP_LOAD_FAILED' | 'RENDER_FAILED' | 'RESOURCE_FAILED' | 'SCENE_NOT_FOUND' | 'SCREENSHOT_FAILED' | 'STYLE_INVALID' | 'SYNTHETIC_ASSET_NOT_FOUND' | 'WORLD_RESOLUTION_FAILED';
82
- type TileflowCapturePhase = 'style-validation' | 'browser-start' | 'resource-load' | 'map-load' | 'map-idle' | 'screenshot';
149
+ type TileflowCaptureErrorCode = 'ABORTED' | 'APPLICATION_ORIGIN_REQUIRED' | 'APPLICATION_ORIGIN_INVALID' | 'APPLICATION_NAVIGATION_FAILED' | 'APPLICATION_TARGET_NOT_FOUND' | 'APPLICATION_TARGET_AMBIGUOUS' | 'APPLICATION_ERROR' | 'BASELINE_INVALID' | 'BROWSER_INSTALL_FAILED' | 'BROWSER_MISSING' | 'BROWSER_START_FAILED' | 'CAPTURE_TIMEOUT' | 'INVALID_PNG' | 'MAP_LOAD_FAILED' | 'RENDER_FAILED' | 'RESOURCE_FAILED' | 'SCENE_NOT_FOUND' | 'SCREENSHOT_FAILED' | 'SHOT_NOT_FOUND' | 'STYLE_INVALID' | 'SYNTHETIC_ASSET_NOT_FOUND' | 'VIDEO_ENCODE_FAILED' | 'VIDEO_ENCODER_UNAVAILABLE' | 'WORLD_RESOLUTION_FAILED';
150
+ type TileflowCapturePhase = 'style-validation' | 'browser-start' | 'resource-load' | 'map-load' | 'theme-blend' | 'map-idle' | 'screenshot' | 'video-encode';
83
151
  type TileflowCaptureDiagnostic = {
84
152
  message: string;
85
153
  path: string;
@@ -106,11 +174,6 @@ declare class TileflowCaptureError extends Error {
106
174
  }
107
175
  declare function captureErrorMessage(error: unknown, fallback: string): string;
108
176
 
109
- declare const tileflowSyntheticAssetOrigin = "https://tileflow.local.invalid";
110
- type TileflowCaptureAsset = TileflowBuildAsset & {
111
- url?: string;
112
- };
113
-
114
177
  declare const tileflowWorldCurrentTileJsonUrl: "https://api.tileflow.dev/tiles/world/tiles.json";
115
178
  type FetchTileJson = (input: string, init: {
116
179
  cache: 'no-store';
@@ -365,4 +428,4 @@ declare const tileflowVisualReviewLimits: Readonly<{
365
428
  maximumAggregatePngBytes: number;
366
429
  }>;
367
430
 
368
- export { type CreateTileflowCaptureSessionOptions, type PreparedTileflowCaptureStyle, type TileflowBrowserInstallProgress, type TileflowBrowserLaunchOptions, type TileflowCapture, TileflowCaptureBrowserManager, TileflowCaptureDataInput, type TileflowCaptureDiagnostic, TileflowCaptureError, type TileflowCaptureErrorCode, type TileflowCaptureErrorDetails, type TileflowCaptureOptions, type TileflowCapturePhase, TileflowCaptureReceipt, TileflowCaptureRendererIdentity, type TileflowCaptureResolvedWorldV1, type TileflowCaptureResourceDiagnostic, type TileflowCaptureResourceKind, type TileflowCaptureResult, type TileflowCaptureSession, TileflowCaptureWorldIdentity, TileflowCaptureWorldSession, type TileflowVisualAppearanceDelta, type TileflowVisualAppearanceProfile, type TileflowVisualAppearanceStatistics, type TileflowVisualBaseline, type TileflowVisualComparison, type TileflowVisualComparisonDocument, type TileflowVisualComparisonStatus, type TileflowVisualImageIdentity, type TileflowVisualPaletteColor, type TileflowVisualPixelMetric, type TileflowVisualReferenceAnalysis, type TileflowVisualReferenceAnalysisDocument, type TileflowVisualReferenceAnalysisOptions, type TileflowVisualReferenceAppearance, type TileflowVisualRegion, type TileflowVisualReviewAppearance, type TileflowVisualReviewCapture, type TileflowVisualReviewComparison, type TileflowVisualReviewDefinition, type TileflowVisualReviewDocument, TileflowVisualReviewError, type TileflowVisualReviewFrameIdentity, type TileflowVisualReviewOptions, type TileflowVisualReviewSide, type TileflowVisualReviewStatus, type TileflowVisualRuntimeIdentity, type TileflowVisualSceneIdentity, analyzeTileflowCaptureReference, captureErrorMessage, captureTileflowScenes, compareTileflowCaptureToBaseline, compareTileflowCapturesForReview, createTileflowCaptureBrowserEnvironment, createTileflowCaptureSession, createTileflowVisualComparisonDocument, createTileflowVisualReferenceAnalysisDocument, createTileflowVisualReviewDocument, launchTileflowCaptureBrowser, resolveTileflowApplicationUrl, resolveTileflowCaptureWorldTileJson, serializeTileflowVisualComparison, setupTileflowCaptureBrowser, tileflowCaptureResultSchemaVersion, tileflowSyntheticAssetOrigin, tileflowVisualArtifactLimits, tileflowVisualComparisonSchemaVersion, tileflowVisualEdgeThreshold, tileflowVisualPerceptualThreshold, tileflowVisualReviewLimits, tileflowVisualReviewSchemaVersion, tileflowWorldCurrentTileJsonUrl, validateTileflowVisualReferencePng };
431
+ export { type CaptureTileflowShotOptions, type CreateTileflowCaptureSessionOptions, type PreparedTileflowCaptureStyle, type TileflowBrowserInstallProgress, type TileflowBrowserLaunchOptions, type TileflowCapture, TileflowCaptureBrowserManager, TileflowCaptureDataInput, type TileflowCaptureDiagnostic, TileflowCaptureError, type TileflowCaptureErrorCode, type TileflowCaptureErrorDetails, type TileflowCaptureOptions, type TileflowCapturePhase, TileflowCaptureReceipt, TileflowCaptureRendererIdentity, type TileflowCaptureResolvedWorldV1, type TileflowCaptureResourceDiagnostic, type TileflowCaptureResourceKind, type TileflowCaptureResult, type TileflowCaptureSession, TileflowCaptureWorldIdentity, TileflowCaptureWorldSession, type TileflowShotCapture, type TileflowShotCaptureOptions, type TileflowShotCaptureResult, type TileflowShotFormat, type TileflowShotFrame, TileflowShotReceiptV1, type TileflowShotVideoCodec, type TileflowVisualAppearanceDelta, type TileflowVisualAppearanceProfile, type TileflowVisualAppearanceStatistics, type TileflowVisualBaseline, type TileflowVisualComparison, type TileflowVisualComparisonDocument, type TileflowVisualComparisonStatus, type TileflowVisualImageIdentity, type TileflowVisualPaletteColor, type TileflowVisualPixelMetric, type TileflowVisualReferenceAnalysis, type TileflowVisualReferenceAnalysisDocument, type TileflowVisualReferenceAnalysisOptions, type TileflowVisualReferenceAppearance, type TileflowVisualRegion, type TileflowVisualReviewAppearance, type TileflowVisualReviewCapture, type TileflowVisualReviewComparison, type TileflowVisualReviewDefinition, type TileflowVisualReviewDocument, TileflowVisualReviewError, type TileflowVisualReviewFrameIdentity, type TileflowVisualReviewOptions, type TileflowVisualReviewSide, type TileflowVisualReviewStatus, type TileflowVisualRuntimeIdentity, type TileflowVisualSceneIdentity, analyzeTileflowCaptureReference, captureErrorMessage, captureTileflowScenes, captureTileflowShot, compareTileflowCaptureToBaseline, compareTileflowCapturesForReview, createTileflowCaptureBrowserEnvironment, createTileflowCaptureSession, createTileflowVisualComparisonDocument, createTileflowVisualReferenceAnalysisDocument, createTileflowVisualReviewDocument, launchTileflowCaptureBrowser, resolveTileflowApplicationUrl, resolveTileflowCaptureWorldTileJson, serializeTileflowVisualComparison, setupTileflowCaptureBrowser, tileflowCaptureResultSchemaVersion, tileflowShotFramesSha256, tileflowSyntheticAssetOrigin, tileflowVisualArtifactLimits, tileflowVisualComparisonSchemaVersion, tileflowVisualEdgeThreshold, tileflowVisualPerceptualThreshold, tileflowVisualReviewLimits, tileflowVisualReviewSchemaVersion, tileflowWorldCurrentTileJsonUrl, validateTileflowVisualReferencePng };