@tileflow/capture 0.1.0-alpha.38 → 0.1.0-alpha.40

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
@@ -256,6 +256,16 @@ retained `definition`; when present, that definition must match the explicit rev
256
256
  `analyzeTileflowCaptureReference` analyzes
257
257
  an external PNG without claiming receipt compatibility. Neither operation approves a baseline.
258
258
 
259
+ ## Complete images and warnings
260
+
261
+ A map whose style declares a 3D scene is captured with it mounted, and each image waits until the
262
+ scene has nothing pending, within `timeoutMs` (30 seconds by default). An image whose scene had not
263
+ settled, a dataset the capture is not entitled to read (HTTP 401 or 403, named in the warning) and a
264
+ missing 3D model leave a drawable map: the capture succeeds and `capture.warnings` says what is
265
+ missing. Set `graphics: 'gpu'` on the session to render with the machine's graphics hardware, much
266
+ faster for dense 3D views but with hardware-dependent pixels; keep the default `software` for
267
+ baselines. `session.contactSheet(images)` lays captured PNGs out in one labelled grid.
268
+
259
269
  ## Errors, receipts, and reproducibility
260
270
 
261
271
  `TileflowCaptureError` reports a stable code, phase, and bounded details. Phases distinguish style
package/dist/index.d.ts CHANGED
@@ -5,14 +5,21 @@ import { NormalizedTileflowCaptureShot, NormalizedTileflowCaptureScene, Tileflow
5
5
  import { TileflowBuildAsset, TileflowIconResolutionOptions, TileflowBuildArtifacts } from '@tileflow/dev/artifacts';
6
6
 
7
7
  type TileflowBrowserInstallProgress = 'installing' | 'installed';
8
+ /**
9
+ * `software` (the default) renders WebGL on the CPU, so images match across machines. `gpu` uses
10
+ * this machine's graphics hardware: many times faster for 3D scenes, but its pixels depend on the
11
+ * hardware and driver, so it suits exploration rather than committed baselines.
12
+ */
13
+ type TileflowCaptureGraphics = 'software' | 'gpu';
8
14
  type TileflowBrowserLaunchOptions = {
9
15
  allowInstall?: boolean;
16
+ graphics?: TileflowCaptureGraphics;
10
17
  onInstallProgress?: (progress: TileflowBrowserInstallProgress) => void;
11
18
  signal?: AbortSignal;
12
19
  };
13
20
  type TileflowCaptureBrowserDependencies = {
14
21
  install(signal?: AbortSignal): Promise<void>;
15
- launch(): Promise<Browser>;
22
+ launch(graphics?: TileflowCaptureGraphics): Promise<Browser>;
16
23
  };
17
24
  declare function createTileflowCaptureBrowserEnvironment(source?: NodeJS.ProcessEnv): Record<string, string>;
18
25
  declare function launchTileflowCaptureBrowser(options?: TileflowBrowserLaunchOptions): Promise<Browser>;
@@ -24,6 +31,34 @@ declare class TileflowCaptureBrowserManager {
24
31
  close(): Promise<void>;
25
32
  }
26
33
 
34
+ /** One captured image on a contact sheet, with the label printed under it. */
35
+ type TileflowContactSheetImage = {
36
+ label: string;
37
+ png: Uint8Array;
38
+ };
39
+ type TileflowContactSheetOptions = {
40
+ /** Images per row; defaults to the square root of the count, at most 4. */
41
+ columns?: number;
42
+ /** Width of each image on the sheet in CSS pixels; defaults to 480. */
43
+ cellWidth?: number;
44
+ };
45
+ declare const tileflowContactSheetLimits: Readonly<{
46
+ cellWidth: {
47
+ maximum: number;
48
+ minimum: number;
49
+ };
50
+ columns: {
51
+ maximum: number;
52
+ minimum: number;
53
+ };
54
+ images: 64;
55
+ }>;
56
+ /**
57
+ * Lays captured images out in one labelled grid PNG, for choosing between candidates at a glance.
58
+ * The sheet renders offline in the given browser: it loads nothing but the images it is given.
59
+ */
60
+ declare function renderTileflowContactSheet(browser: Browser, images: readonly TileflowContactSheetImage[], options?: TileflowContactSheetOptions): Promise<Uint8Array>;
61
+
27
62
  declare const tileflowSyntheticAssetOrigin = "https://tileflow.local.invalid";
28
63
  type TileflowCaptureAsset = TileflowBuildAsset & {
29
64
  url?: string;
@@ -83,6 +118,8 @@ type TileflowCaptureOptions = {
83
118
  icons?: TileflowIconResolutionOptions;
84
119
  onBrowserInstallProgress?: (progress: TileflowBrowserInstallProgress) => void;
85
120
  frame?: 'map' | 'viewport';
121
+ /** How the browser renders WebGL; see `TileflowCaptureGraphics`. Defaults to `software`. */
122
+ graphics?: TileflowCaptureGraphics;
86
123
  scenes: string[];
87
124
  signal?: AbortSignal;
88
125
  selector?: string;
@@ -139,6 +176,10 @@ type TileflowCaptureSession = {
139
176
  /** Renders committed shots in name order from one compiled artifact snapshot. */
140
177
  captureShots(shots: string[], options?: TileflowShotCaptureOptions): Promise<TileflowShotCaptureResult>;
141
178
  captureAllShots(options?: TileflowShotCaptureOptions): Promise<TileflowShotCaptureResult>;
179
+ /** Lays captured images out in one labelled grid PNG, with this session's browser. */
180
+ contactSheet(images: readonly TileflowContactSheetImage[], options?: TileflowContactSheetOptions & {
181
+ signal?: AbortSignal;
182
+ }): Promise<Uint8Array>;
142
183
  close(): Promise<void>;
143
184
  };
144
185
  declare function captureTileflowScenes(options: TileflowCaptureOptions): Promise<TileflowCaptureResult>;
@@ -428,4 +469,4 @@ declare const tileflowVisualReviewLimits: Readonly<{
428
469
  maximumAggregatePngBytes: number;
429
470
  }>;
430
471
 
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 };
472
+ export { type CaptureTileflowShotOptions, type CreateTileflowCaptureSessionOptions, type PreparedTileflowCaptureStyle, type TileflowBrowserInstallProgress, type TileflowBrowserLaunchOptions, type TileflowCapture, TileflowCaptureBrowserManager, TileflowCaptureDataInput, type TileflowCaptureDiagnostic, TileflowCaptureError, type TileflowCaptureErrorCode, type TileflowCaptureErrorDetails, type TileflowCaptureGraphics, type TileflowCaptureOptions, type TileflowCapturePhase, TileflowCaptureReceipt, TileflowCaptureRendererIdentity, type TileflowCaptureResolvedWorldV1, type TileflowCaptureResourceDiagnostic, type TileflowCaptureResourceKind, type TileflowCaptureResult, type TileflowCaptureSession, TileflowCaptureWorldIdentity, TileflowCaptureWorldSession, type TileflowContactSheetImage, type TileflowContactSheetOptions, 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, renderTileflowContactSheet, resolveTileflowApplicationUrl, resolveTileflowCaptureWorldTileJson, serializeTileflowVisualComparison, setupTileflowCaptureBrowser, tileflowCaptureResultSchemaVersion, tileflowContactSheetLimits, tileflowShotFramesSha256, tileflowSyntheticAssetOrigin, tileflowVisualArtifactLimits, tileflowVisualComparisonSchemaVersion, tileflowVisualEdgeThreshold, tileflowVisualPerceptualThreshold, tileflowVisualReviewLimits, tileflowVisualReviewSchemaVersion, tileflowWorldCurrentTileJsonUrl, validateTileflowVisualReferencePng };
package/dist/index.js CHANGED
@@ -116,7 +116,7 @@ async function launchTileflowCaptureBrowser(options = {}) {
116
116
  async function launchTileflowCaptureBrowserWithDependencies(options, dependencies) {
117
117
  throwIfAborted(options.signal);
118
118
  try {
119
- return await dependencies.launch();
119
+ return await dependencies.launch(options.graphics);
120
120
  } catch (error) {
121
121
  if (!isMissingBrowserError(error)) {
122
122
  throw new TileflowCaptureError(
@@ -137,7 +137,7 @@ async function launchTileflowCaptureBrowserWithDependencies(options, dependencie
137
137
  throwIfAborted(options.signal);
138
138
  options.onInstallProgress?.("installed");
139
139
  try {
140
- return await dependencies.launch();
140
+ return await dependencies.launch(options.graphics);
141
141
  } catch (retryError) {
142
142
  throw new TileflowCaptureError(
143
143
  "BROWSER_START_FAILED",
@@ -224,11 +224,21 @@ var windowsChromiumDisabledFeatures = [
224
224
  // Avoid intermittent Windows loopback failures: https://issues.chromium.org/issues/572515601.
225
225
  "TcpPortRandomizationWin"
226
226
  ];
227
- function launchPinnedBrowser() {
227
+ function tileflowGpuBrowserArguments(platform = process.platform) {
228
+ const backend = platform === "darwin" ? "metal" : platform === "win32" ? "d3d11" : "vulkan";
229
+ return [
230
+ "--enable-gpu",
231
+ "--ignore-gpu-blocklist",
232
+ `--use-angle=${backend}`,
233
+ ...platform === "linux" ? ["--enable-features=Vulkan"] : []
234
+ ];
235
+ }
236
+ function launchPinnedBrowser(graphics = "software") {
228
237
  return chromium.launch({
229
238
  args: [
230
239
  "--disable-dev-shm-usage",
231
- ...process.platform === "win32" ? [`--disable-features=${windowsChromiumDisabledFeatures.join(",")}`] : []
240
+ ...process.platform === "win32" ? [`--disable-features=${windowsChromiumDisabledFeatures.join(",")}`] : [],
241
+ ...graphics === "gpu" ? tileflowGpuBrowserArguments() : []
232
242
  ],
233
243
  chromiumSandbox: true,
234
244
  env: createTileflowCaptureBrowserEnvironment(),
@@ -347,6 +357,12 @@ import { readFileSync as readFileSync2 } from "fs";
347
357
  import { open } from "fs/promises";
348
358
  import { createRequire as createRequire2 } from "module";
349
359
  import { dirname as dirname2, join as join2 } from "path";
360
+ import {
361
+ tileflowBuildingsMetadataKey,
362
+ tileflowStreetLightsMetadataKey,
363
+ tileflowVegetationDisplayMetadataKey,
364
+ tileflowVegetationMetadataKey
365
+ } from "@tileflow/core";
350
366
  import { getTileflowStyleFontFaces } from "@tileflow/core/runtime";
351
367
  import {
352
368
  createTileflowLineBackgroundGlyphResponder,
@@ -374,10 +390,12 @@ globalThis.__tileflowRegisterContourProtocol = registerTileflowContourProtocol;
374
390
  globalThis.__tileflowRegisterPathGroundProtocol = registerTileflowPathGroundProtocol;
375
391
  globalThis.__tileflowRegisterPmtilesProtocol = registerTileflowPmtilesProtocol;
376
392
  globalThis.__tileflowAttachAtmosphere = attachTileflowAtmosphere;
393
+ globalThis.__tileflowAttachBranding = attachTileflowBranding;
377
394
  globalThis.__tileflowAttachNotices = attachTileflowNotices;
378
395
  globalThis.__tileflowAttachNauticalLights = attachTileflowNauticalLights;
379
396
  globalThis.__tileflowMaxPitch = tileflowMaxPitch;
380
397
  globalThis.__tileflowAttachLabelPlacement = attachTileflowLabelPlacement;
398
+ globalThis.__tileflowCreateMap3D = createTileflowMap3D;
381
399
  globalThis.__tileflowCreateThemeController = createTileflowThemeController;`;
382
400
  async function captureStandaloneTileflowScene(input) {
383
401
  if (input.scene.target.kind !== "map") {
@@ -429,6 +447,8 @@ async function withStandaloneTileflowPage(input, run) {
429
447
  let context;
430
448
  let phase = "browser-start";
431
449
  let timeout;
450
+ let budgetStartedAt = Date.now();
451
+ const pageWarnings = /* @__PURE__ */ new Set();
432
452
  const onAbort = () => {
433
453
  termination = "aborted";
434
454
  void context?.close().catch(() => void 0);
@@ -439,6 +459,7 @@ async function withStandaloneTileflowPage(input, run) {
439
459
  };
440
460
  const restartTimeout = () => {
441
461
  stopTimeout();
462
+ budgetStartedAt = Date.now();
442
463
  timeout = setTimeout(() => {
443
464
  termination = "timeout";
444
465
  void context?.close().catch(() => void 0);
@@ -483,6 +504,11 @@ async function withStandaloneTileflowPage(input, run) {
483
504
  const resourceFailures = [];
484
505
  let pageFailed = false;
485
506
  const recordResourceFailure = (url, status) => {
507
+ const degraded = describeDegradedResource(url, status);
508
+ if (degraded) {
509
+ if (pageWarnings.size < 32) pageWarnings.add(degraded);
510
+ return;
511
+ }
486
512
  const diagnostic = classifyResource(url, status);
487
513
  if (!diagnostic || resourceFailures.length >= 16) return;
488
514
  const key = JSON.stringify(diagnostic);
@@ -590,7 +616,10 @@ async function withStandaloneTileflowPage(input, run) {
590
616
  recordResourceFailure(response.url(), response.status());
591
617
  }
592
618
  });
593
- page.on("requestfailed", (request) => recordResourceFailure(request.url()));
619
+ page.on("requestfailed", (request) => {
620
+ if (request.failure()?.errorText === "net::ERR_ABORTED") return;
621
+ recordResourceFailure(request.url());
622
+ });
594
623
  page.setDefaultTimeout(timeoutMs);
595
624
  await page.goto(mapLibreRuntimeUrls.document, { waitUntil: "domcontentloaded" });
596
625
  await page.addStyleTag({ url: mapLibreRuntimeUrls.css });
@@ -605,16 +634,31 @@ globalThis.maplibregl = maplibregl;`,
605
634
  );
606
635
  await page.addScriptTag({ content: tileflowBrowserModule, type: "module" });
607
636
  await page.addScriptTag({ content: renderMapInPageScript });
637
+ if (input.graphics === "gpu") {
638
+ const renderer = await page.evaluate(() => {
639
+ const context2 = document.createElement("canvas").getContext("webgl2");
640
+ const info = context2?.getExtension("WEBGL_debug_renderer_info");
641
+ return info ? String(context2.getParameter(info.UNMASKED_RENDERER_WEBGL)) : "";
642
+ });
643
+ pageWarnings.add(
644
+ /swiftshader|llvmpipe|software/iu.test(renderer) || renderer === "" ? "GPU rendering is unavailable on this machine; the capture used software rendering." : "Rendered with this machine's GPU: pixels depend on its hardware and driver, so compare it only with other GPU captures from the same machine."
645
+ );
646
+ }
608
647
  const step = async (stepPhase, name, value) => {
609
648
  phase = stepPhase;
649
+ const reserveMs = Math.max(1e4, Math.round(timeoutMs * 0.35));
650
+ const sceneBudgetMs = Math.max(0, timeoutMs - (Date.now() - budgetStartedAt) - reserveMs);
610
651
  const result = await page.evaluate(
611
652
  ({ name: functionName, value: stepInput }) => {
612
653
  const renderWindow = window;
613
654
  return renderWindow[functionName](stepInput);
614
655
  },
615
- { name, value }
656
+ { name, value: { ...value, sceneBudgetMs } }
616
657
  );
617
658
  assertPhaseSucceeded(result, stepPhase, syntheticAssetFailure, resourceFailures, pageFailed);
659
+ if (result.status === "ok") {
660
+ for (const warning of result.warnings ?? []) pageWarnings.add(safeSceneWarning(warning));
661
+ }
618
662
  };
619
663
  return await run({
620
664
  page,
@@ -662,7 +706,10 @@ globalThis.maplibregl = maplibregl;`,
662
706
  result() {
663
707
  return {
664
708
  networkDependent: remoteOrigins.size > 0,
665
- warnings: [...remoteOrigins].sort(compareCodeUnits).map((origin) => `Capture requested remote resources from ${origin}.`)
709
+ warnings: [
710
+ ...[...remoteOrigins].sort(compareCodeUnits).map((origin) => `Capture requested remote resources from ${origin}.`),
711
+ ...[...pageWarnings].sort(compareCodeUnits)
712
+ ]
666
713
  };
667
714
  }
668
715
  });
@@ -671,10 +718,11 @@ globalThis.maplibregl = maplibregl;`,
671
718
  throw new TileflowCaptureError("ABORTED", "Tileflow capture was aborted.", { cause: error });
672
719
  }
673
720
  if (termination === "timeout") {
674
- throw new TileflowCaptureError("CAPTURE_TIMEOUT", timeoutMessage(phase), {
675
- cause: error,
676
- details: { phase }
677
- });
721
+ throw new TileflowCaptureError(
722
+ "CAPTURE_TIMEOUT",
723
+ `${timeoutMessage(phase)}${slowSceneHint(input.style, input.graphics, phase)}`,
724
+ { cause: error, details: { phase } }
725
+ );
678
726
  }
679
727
  if (error instanceof CallerFailure) {
680
728
  throw error.error;
@@ -925,6 +973,41 @@ function classifyResource(value, status) {
925
973
  ...status === void 0 ? {} : { status }
926
974
  };
927
975
  }
976
+ function describeDegradedResource(value, status) {
977
+ if (status === void 0) return void 0;
978
+ let url;
979
+ try {
980
+ url = new URL(value);
981
+ } catch {
982
+ return void 0;
983
+ }
984
+ if (url.protocol !== "http:" && url.protocol !== "https:") return void 0;
985
+ const kind = classifyResource(value, status)?.kind;
986
+ if (kind === "glyph" || kind === "sprite-json" || kind === "sprite-image") return void 0;
987
+ const dataset = tileflowDatasetOf(url);
988
+ if (status === 401 || status === 403) {
989
+ const subject = dataset ? `Tileflow dataset "${dataset}"` : `Data from ${url.origin}`;
990
+ return `${subject} returned HTTP ${status}: this capture is not entitled to read it, so its layers are missing from the image.`;
991
+ }
992
+ if (status === 404 && isModelResource(url)) {
993
+ return `A 3D model from ${dataset ? `Tileflow dataset "${dataset}"` : url.origin} returned HTTP 404; the buildings under it were captured instead.`;
994
+ }
995
+ return void 0;
996
+ }
997
+ function tileflowDatasetOf(url) {
998
+ const match = /^\/(?:v1\/)?tiles\/([^/]+)\//u.exec(url.pathname);
999
+ if (!match) return void 0;
1000
+ try {
1001
+ const name = decodeURIComponent(match[1]);
1002
+ return /^[a-z0-9][a-z0-9,_-]{0,63}$/u.test(name) ? name : void 0;
1003
+ } catch {
1004
+ return void 0;
1005
+ }
1006
+ }
1007
+ function isModelResource(url) {
1008
+ const path = url.pathname.toLowerCase();
1009
+ return path.endsWith(".glb") || /\/(?:landmarks|models)(?:\/|$)/u.test(path);
1010
+ }
928
1011
  function fontStackContext(path) {
929
1012
  const segments = path.split("/").filter(Boolean);
930
1013
  const index = segments.findIndex((segment) => /^(?:fonts?|glyphs?)$/i.test(segment));
@@ -951,6 +1034,20 @@ function resourceLabel(kind) {
951
1034
  "vector-tile": "Vector tile"
952
1035
  }[kind];
953
1036
  }
1037
+ var sceneMetadataKeys = [
1038
+ tileflowBuildingsMetadataKey,
1039
+ tileflowVegetationMetadataKey,
1040
+ tileflowVegetationDisplayMetadataKey,
1041
+ tileflowStreetLightsMetadataKey
1042
+ ];
1043
+ function slowSceneHint(style, graphics, phase) {
1044
+ if (graphics === "gpu" || phase !== "map-idle" && phase !== "screenshot") return "";
1045
+ const metadata = style.metadata;
1046
+ if (!sceneMetadataKeys.some((key) => metadata?.[key] !== void 0 && metadata[key] !== null)) {
1047
+ return "";
1048
+ }
1049
+ return " Software rendering draws a dense 3D scene slowly: raise --timeout, or pass --gpu for exploratory captures.";
1050
+ }
954
1051
  function timeoutMessage(phase) {
955
1052
  if (phase === "map-load") return "MapLibre did not finish loading before the capture timeout.";
956
1053
  if (phase === "theme-blend") return "The theme blend was not ready before the capture timeout.";
@@ -966,6 +1063,9 @@ function mapFailureMessage(phase) {
966
1063
  function safeMapError(value) {
967
1064
  return value.replace(/[A-Za-z][A-Za-z0-9+.-]*:\/\/\S+/g, "resource").replace(/[^A-Za-z0-9 _.,:'"@+()[\]/=-]/g, "").trim().slice(0, 200);
968
1065
  }
1066
+ function safeSceneWarning(value) {
1067
+ return value.replace(/[A-Za-z][A-Za-z0-9+.-]*:\/\/\S+/g, "resource").replace(/[^A-Za-z0-9 _.,:;'"@+()[\]/=-]/g, "").trim().slice(0, 300);
1068
+ }
969
1069
  function safeAssetName(value) {
970
1070
  return value.replace(/[^A-Za-z0-9._/-]/g, "").slice(0, 200) || "unknown";
971
1071
  }
@@ -1043,10 +1143,22 @@ window.__tileflowCaptureLoad = async function(input) {
1043
1143
  window.__tileflowCaptureStyle = input.style;
1044
1144
  window.__tileflowAttachAtmosphere(map);
1045
1145
  // Notices such as "Not for navigation" are part of the map, so captured images keep them.
1146
+ window.__tileflowAttachBranding(map);
1046
1147
  window.__tileflowAttachNotices(map);
1047
1148
  // Capture prefers reduced motion, so Nautical lights stay steady and every image is stable.
1048
1149
  window.__tileflowAttachNauticalLights(map);
1049
1150
  window.__tileflowAttachLabelPlacement(map);
1151
+ // The 3D scene the style declares (buildings, landmarks and Team models, trees, street
1152
+ // lights), mounted as an application and the preview mount it, so captures show the same map.
1153
+ window.__tileflowSceneErrors = [];
1154
+ window.__tileflowScene = window.__tileflowCreateMap3D?.({
1155
+ map,
1156
+ onError: (error) => {
1157
+ if (window.__tileflowSceneErrors.length < 4) {
1158
+ window.__tileflowSceneErrors.push(String(error?.message || error || "unknown error"));
1159
+ }
1160
+ }
1161
+ });
1050
1162
  return await waitForMapEvent(map, "load", input.mapEventTimeoutMs);
1051
1163
  } catch (error) {
1052
1164
  return {status: "failed", reason: "error", message: String(error?.message || error || "")};
@@ -1104,14 +1216,11 @@ async function loadTileflowFontFaces(definitions) {
1104
1216
  window.__tileflowCaptureIdle = async function(input) {
1105
1217
  const map = window.__tileflowMap;
1106
1218
  if (!map) return {status: "failed", reason: "error"};
1219
+ releaseFrames(map);
1107
1220
 
1108
1221
  try {
1109
1222
  applyCamera(map, input.camera);
1110
- const result = await waitForMapEvent(map, "idle", input.mapEventTimeoutMs);
1111
- if (result.status === "failed") return result;
1112
- await nextFrame();
1113
- await nextFrame();
1114
- return {status: "ok"};
1223
+ return await settleMap(map, input);
1115
1224
  } catch {
1116
1225
  return {status: "failed", reason: "error"};
1117
1226
  }
@@ -1121,6 +1230,7 @@ window.__tileflowCaptureIdle = async function(input) {
1121
1230
  window.__tileflowCapturePrepareBlend = async function(input) {
1122
1231
  const map = window.__tileflowMap;
1123
1232
  if (!map) return {status: "failed", reason: "error"};
1233
+ releaseFrames(map);
1124
1234
 
1125
1235
  try {
1126
1236
  const controller = window.__tileflowCreateThemeController({
@@ -1145,6 +1255,7 @@ window.__tileflowCapturePrepareBlend = async function(input) {
1145
1255
  window.__tileflowCaptureFrame = async function(input) {
1146
1256
  const map = window.__tileflowMap;
1147
1257
  if (!map) return {status: "failed", reason: "error"};
1258
+ releaseFrames(map);
1148
1259
 
1149
1260
  try {
1150
1261
  const blend = window.__tileflowShotBlend;
@@ -1159,17 +1270,110 @@ window.__tileflowCaptureFrame = async function(input) {
1159
1270
  }
1160
1271
  applyCamera(map, input.camera);
1161
1272
  map.triggerRepaint();
1273
+ return await settleMap(map, input);
1274
+ } catch {
1275
+ return {status: "failed", reason: "error"};
1276
+ }
1277
+ };
1278
+
1279
+ // MapLibre's idle covers the style's own sources only, and a 3D scene still preparing keeps asking
1280
+ // for frames, so on a slow renderer the map may not become idle. With a 3D scene the capture waits until
1281
+ // the map's tiles have loaded and the scene has nothing pending, across a few consecutive checks.
1282
+ async function settleMap(map, input) {
1283
+ const scene = window.__tileflowScene;
1284
+ if (!scene) {
1162
1285
  const result = await waitForMapEvent(map, "idle", input.mapEventTimeoutMs);
1163
1286
  if (result.status === "failed") return result;
1164
1287
  await nextFrame();
1165
1288
  await nextFrame();
1166
- return {status: "ok"};
1167
- } catch {
1168
- return {status: "failed", reason: "error"};
1289
+ return {status: "ok", warnings: []};
1169
1290
  }
1170
- };
1291
+ const warnings = [];
1292
+ const budget = Math.max(0, Math.min(input.mapEventTimeoutMs, input.sceneBudgetMs ?? 0));
1293
+ const deadline = performance.now() + budget;
1294
+ const failure = watchMapErrors(map);
1295
+ let quiet = 0;
1296
+ let pending = scene.pending();
1297
+ try {
1298
+ // The scene asks for the frames it needs; forcing more would only slow a software renderer.
1299
+ while (quiet < 3 && performance.now() < deadline) {
1300
+ await delay(100);
1301
+ await nextFrame();
1302
+ if (failure.result) return failure.result;
1303
+ pending = scene.pending();
1304
+ const busy = pending.buildingTiles + pending.models + pending.treeTiles + pending.transitions;
1305
+ quiet = busy === 0 && map.loaded() ? quiet + 1 : 0;
1306
+ }
1307
+ } finally {
1308
+ failure.stop();
1309
+ }
1310
+ if (quiet < 3) {
1311
+ const parts = [];
1312
+ if (!map.loaded()) parts.push("map tiles");
1313
+ if (pending.buildingTiles) parts.push(pending.buildingTiles + " building tiles");
1314
+ if (pending.models) parts.push(pending.models + " models");
1315
+ if (pending.treeTiles) parts.push(pending.treeTiles + " tree tiles");
1316
+ if (pending.transitions) parts.push(pending.transitions + " transitions");
1317
+ // Map tiles still loading are a MapLibre failure to settle, as without a 3D scene.
1318
+ if (!map.loaded() && parts.length === 1) return {status: "failed", reason: "timeout"};
1319
+ warnings.push(
1320
+ "The 3D scene was still loading when the capture budget ran out (" +
1321
+ parts.join(", ") +
1322
+ " pending after " + Math.round(budget / 1000) + " s); the image may be incomplete. Raise --timeout, or pass --gpu for exploratory captures."
1323
+ );
1324
+ }
1325
+ for (const message of window.__tileflowSceneErrors || []) {
1326
+ warnings.push("The 3D scene failed to draw (" + message + "); the flat map was captured instead.");
1327
+ }
1328
+ if (quiet >= 3) {
1329
+ map.triggerRepaint();
1330
+ await nextFrame();
1331
+ await nextFrame();
1332
+ }
1333
+ holdFrames(map);
1334
+ return {status: "ok", warnings};
1335
+ }
1336
+
1337
+ // Keeps the last frame on screen while the PNG is taken: on a software renderer a 3D scene that
1338
+ // is still preparing would otherwise queue frames that take seconds each and stall the screenshot.
1339
+ // The next frame step releases it.
1340
+ function holdFrames(map) {
1341
+ if (map.__tileflowHeldRepaint) return;
1342
+ map.__tileflowHeldRepaint = map.triggerRepaint;
1343
+ map.triggerRepaint = () => map;
1344
+ }
1171
1345
 
1346
+ function releaseFrames(map) {
1347
+ if (!map.__tileflowHeldRepaint) return;
1348
+ map.triggerRepaint = map.__tileflowHeldRepaint;
1349
+ map.__tileflowHeldRepaint = undefined;
1350
+ }
1351
+
1352
+ // Reports the first map error while the scene settles, with the idle wait's tolerance for source
1353
+ // layers a TileJSON advertises but one tile lacks.
1354
+ function watchMapErrors(map) {
1355
+ const state = {result: undefined, stop: () => undefined};
1356
+ const pending = waitForMapEvent(map, "tileflow:never", 2147483647);
1357
+ pending.then((result) => {
1358
+ if (result.status === "failed" && result.reason === "error") state.result = result;
1359
+ });
1360
+ state.stop = () => pending.cancel?.();
1361
+ return state;
1362
+ }
1363
+
1364
+ function delay(milliseconds) {
1365
+ return new Promise((resolve) => window.setTimeout(resolve, milliseconds));
1366
+ }
1367
+
1368
+ // A map's pitch cap can depend on zoom (a 3D scene lowers it over the overview), so a camera
1369
+ // that changes both is applied again once the new zoom has lifted the cap.
1172
1370
  function applyCamera(mapInstance, camera) {
1371
+ moveCamera(mapInstance, camera);
1372
+ const pitch = camera.pitch ?? 0;
1373
+ if (Math.abs(mapInstance.getPitch() - pitch) > 1e-6) moveCamera(mapInstance, camera);
1374
+ }
1375
+
1376
+ function moveCamera(mapInstance, camera) {
1173
1377
  if (camera.type === "bounds") {
1174
1378
  mapInstance.fitBounds(
1175
1379
  [
@@ -1195,7 +1399,8 @@ function applyCamera(mapInstance, camera) {
1195
1399
  }
1196
1400
 
1197
1401
  function waitForMapEvent(mapInstance, event, timeoutMs) {
1198
- return new Promise((resolve) => {
1402
+ let cancel;
1403
+ const promise = new Promise((resolve) => {
1199
1404
  const timeout = window.setTimeout(() => {
1200
1405
  cleanup();
1201
1406
  resolve({status: "failed", reason: "timeout"});
@@ -1205,6 +1410,15 @@ function waitForMapEvent(mapInstance, event, timeoutMs) {
1205
1410
  resolve({status: "ok"});
1206
1411
  };
1207
1412
  const onError = (errorEvent) => {
1413
+ // Data the key cannot read leaves the rest of the map drawable; the capture reports it as a
1414
+ // warning naming the dataset instead of failing.
1415
+ const status = errorEvent?.error?.status;
1416
+ if (status === 401 || status === 403) {
1417
+ // MapLibre never finishes loading a style whose source it could not read; the source and
1418
+ // its layers come off, so the rest of the map loads and draws.
1419
+ if (errorEvent?.sourceId) removeRefusedSource(mapInstance, errorEvent.sourceId);
1420
+ return;
1421
+ }
1208
1422
  const message = String(errorEvent?.error?.message || errorEvent?.message || "");
1209
1423
  const missingSourceLayer = message.match(
1210
1424
  /^Source layer "(.+)" does not exist on source "(.+)" as specified by style layer ".+"\.$/
@@ -1234,9 +1448,27 @@ function waitForMapEvent(mapInstance, event, timeoutMs) {
1234
1448
  mapInstance.off("error", onError);
1235
1449
  };
1236
1450
 
1451
+ cancel = () => {
1452
+ cleanup();
1453
+ resolve({status: "cancelled"});
1454
+ };
1237
1455
  mapInstance.once(event, onSuccess);
1238
1456
  mapInstance.on("error", onError);
1239
1457
  });
1458
+ promise.cancel = cancel;
1459
+ return promise;
1460
+ }
1461
+
1462
+ function removeRefusedSource(mapInstance, sourceId) {
1463
+ try {
1464
+ if (!mapInstance.getSource(sourceId)) return;
1465
+ for (const layer of mapInstance.getStyle()?.layers || []) {
1466
+ if (layer.source === sourceId) mapInstance.removeLayer(layer.id);
1467
+ }
1468
+ mapInstance.removeSource(sourceId);
1469
+ } catch {
1470
+ // The warning still names the dataset; MapLibre reports anything else it cannot load.
1471
+ }
1240
1472
  }
1241
1473
 
1242
1474
  function isLoopbackSourceUrl(value) {
@@ -1656,6 +1888,90 @@ function compareCodeUnits2(left, right) {
1656
1888
  return left < right ? -1 : left > right ? 1 : 0;
1657
1889
  }
1658
1890
 
1891
+ // src/contact-sheet.ts
1892
+ var tileflowContactSheetLimits = Object.freeze({
1893
+ cellWidth: { maximum: 1024, minimum: 120 },
1894
+ columns: { maximum: 8, minimum: 1 },
1895
+ images: 64
1896
+ });
1897
+ async function renderTileflowContactSheet(browser, images, options = {}) {
1898
+ const limits = tileflowContactSheetLimits;
1899
+ if (images.length === 0 || images.length > limits.images) {
1900
+ throw new TileflowCaptureError(
1901
+ "SCREENSHOT_FAILED",
1902
+ `A contact sheet holds from 1 to ${limits.images} images.`
1903
+ );
1904
+ }
1905
+ const columns = Math.round(options.columns ?? Math.min(4, Math.ceil(Math.sqrt(images.length))));
1906
+ const cellWidth = Math.round(options.cellWidth ?? 480);
1907
+ if (!(columns >= limits.columns.minimum && columns <= limits.columns.maximum) || !(cellWidth >= limits.cellWidth.minimum && cellWidth <= limits.cellWidth.maximum)) {
1908
+ throw new TileflowCaptureError(
1909
+ "SCREENSHOT_FAILED",
1910
+ `Contact sheet columns must be ${limits.columns.minimum}-${limits.columns.maximum} and cell width ${limits.cellWidth.minimum}-${limits.cellWidth.maximum}.`
1911
+ );
1912
+ }
1913
+ const cells = images.map(({ label, png }) => {
1914
+ const { height: height2, width: width2 } = readPngDimensions(png);
1915
+ return {
1916
+ height: Math.round(cellWidth * height2 / width2),
1917
+ label,
1918
+ src: `data:image/png;base64,${Buffer.from(png).toString("base64")}`
1919
+ };
1920
+ });
1921
+ const gap = 12;
1922
+ const labelHeight = 22;
1923
+ const rows = Math.ceil(cells.length / columns);
1924
+ const rowHeights = Array.from(
1925
+ { length: rows },
1926
+ (_, row) => Math.max(...cells.slice(row * columns, row * columns + columns).map((cell) => cell.height))
1927
+ );
1928
+ const width = gap + columns * (cellWidth + gap);
1929
+ const height = gap + rowHeights.reduce((sum, rowHeight) => sum + rowHeight + labelHeight + gap, 0);
1930
+ const context = await browser.newContext({
1931
+ deviceScaleFactor: 1,
1932
+ javaScriptEnabled: false,
1933
+ serviceWorkers: "block",
1934
+ viewport: { height: Math.min(height, 16384), width }
1935
+ });
1936
+ try {
1937
+ await context.route("**/*", (route) => route.abort("blockedbyclient"));
1938
+ const page = await context.newPage();
1939
+ await page.setContent(contactSheetHtml(cells, { cellWidth, columns, gap, labelHeight }), {
1940
+ waitUntil: "load"
1941
+ });
1942
+ return new Uint8Array(await page.screenshot({ fullPage: true, type: "png" }));
1943
+ } catch (error) {
1944
+ throw new TileflowCaptureError(
1945
+ "SCREENSHOT_FAILED",
1946
+ "Tileflow could not render the contact sheet.",
1947
+ {
1948
+ cause: error,
1949
+ details: { phase: "screenshot" }
1950
+ }
1951
+ );
1952
+ } finally {
1953
+ await context.close().catch(() => void 0);
1954
+ }
1955
+ }
1956
+ function contactSheetHtml(cells, layout) {
1957
+ const figures = cells.map(
1958
+ (cell) => `<figure><img src="${cell.src}" width="${layout.cellWidth}" height="${cell.height}" alt=""><figcaption>${escapeHtml(cell.label)}</figcaption></figure>`
1959
+ ).join("");
1960
+ return `<!doctype html><html><head><meta charset="utf-8"><style>
1961
+ html,body{margin:0;background:#f4f2ee;}
1962
+ main{display:grid;grid-template-columns:repeat(${layout.columns},${layout.cellWidth}px);gap:${layout.gap}px;padding:${layout.gap}px;align-items:start;}
1963
+ figure{margin:0;}
1964
+ img{display:block;border-radius:4px;}
1965
+ figcaption{height:${layout.labelHeight}px;line-height:${layout.labelHeight}px;font:500 13px/22px ui-monospace,Menlo,Consolas,monospace;color:#2b2824;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
1966
+ </style></head><body><main>${figures}</main></body></html>`;
1967
+ }
1968
+ function escapeHtml(value) {
1969
+ return value.replace(
1970
+ /[&<>"']/gu,
1971
+ (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]
1972
+ );
1973
+ }
1974
+
1659
1975
  // src/shot.ts
1660
1976
  import {
1661
1977
  serializeCanonicalJson,
@@ -2608,6 +2924,7 @@ var TileflowCaptureSessionImpl = class {
2608
2924
  this.#options = options;
2609
2925
  this.#browserManager = new TileflowCaptureBrowserManager({
2610
2926
  allowInstall: options.allowBrowserInstall,
2927
+ ...options.graphics ? { graphics: options.graphics } : {},
2611
2928
  onInstallProgress: options.onBrowserInstallProgress,
2612
2929
  signal: options.signal
2613
2930
  });
@@ -2719,6 +3036,7 @@ var TileflowCaptureSessionImpl = class {
2719
3036
  glyphCacheDirectory: resolve(this.#options.cwd ?? process.cwd()),
2720
3037
  lineBackgroundGlyphs: artifacts.lineBackgroundGlyphs
2721
3038
  } : {},
3039
+ ...this.#options.graphics ? { graphics: this.#options.graphics } : {},
2722
3040
  scene,
2723
3041
  signal,
2724
3042
  style,
@@ -2858,6 +3176,12 @@ var TileflowCaptureSessionImpl = class {
2858
3176
  }
2859
3177
  return { schemaVersion: tileflowCaptureResultSchemaVersion, shots };
2860
3178
  }
3179
+ async contactSheet(images, options = {}) {
3180
+ this.#assertOpen();
3181
+ const { signal, ...layout } = options;
3182
+ const browser = await this.#browserManager.getBrowser(signal ?? this.#options.signal);
3183
+ return await renderTileflowContactSheet(browser, images, layout);
3184
+ }
2861
3185
  async close() {
2862
3186
  if (this.#closed) {
2863
3187
  return;
@@ -3217,8 +3541,8 @@ async function analyzeTileflowCaptureReference(actualCapture, referencePng, opti
3217
3541
  };
3218
3542
  }
3219
3543
  function createTileflowVisualReferenceAnalysisDocument(analysis) {
3220
- const { diffPng: _diffPng, ...document } = analysis;
3221
- return document;
3544
+ const { diffPng: _diffPng, ...document2 } = analysis;
3545
+ return document2;
3222
3546
  }
3223
3547
  async function compareTileflowCaptureToBaseline(actualCapture, baseline) {
3224
3548
  const actualReceipt = validateTileflowCaptureReceipt(actualCapture.receipt);
@@ -3361,8 +3685,8 @@ async function compareTileflowCaptureToBaseline(actualCapture, baseline) {
3361
3685
  };
3362
3686
  }
3363
3687
  function createTileflowVisualComparisonDocument(comparison) {
3364
- const { diffPng: _diffPng, ...document } = comparison;
3365
- return document;
3688
+ const { diffPng: _diffPng, ...document2 } = comparison;
3689
+ return document2;
3366
3690
  }
3367
3691
  function serializeTileflowVisualComparison(comparison) {
3368
3692
  return `${serializeCanonicalJson3(createTileflowVisualComparisonDocument(comparison))}
@@ -3794,8 +4118,8 @@ async function compareTileflowCapturesForReview(leftInput, rightInput, options =
3794
4118
  };
3795
4119
  }
3796
4120
  function createTileflowVisualReviewDocument(comparison) {
3797
- const { diffPng: _diffPng, ...document } = comparison;
3798
- return document;
4121
+ const { diffPng: _diffPng, ...document2 } = comparison;
4122
+ return document2;
3799
4123
  }
3800
4124
  var tileflowVisualReviewLimits = Object.freeze({
3801
4125
  maximumAggregatePngBytes: tileflowVisualArtifactLimits.maximumPngBytes
@@ -4185,6 +4509,7 @@ export {
4185
4509
  launchTileflowCaptureBrowser,
4186
4510
  parseTileflowCaptureReceipt,
4187
4511
  parseTileflowShotReceipt,
4512
+ renderTileflowContactSheet,
4188
4513
  resolveTileflowApplicationUrl,
4189
4514
  resolveTileflowCaptureWorldTileJson,
4190
4515
  serializeTileflowCaptureReceipt,
@@ -4195,6 +4520,7 @@ export {
4195
4520
  tileflowCaptureReceiptSchemaVersion,
4196
4521
  tileflowCaptureResultSchemaVersion,
4197
4522
  tileflowCaptureRuntime,
4523
+ tileflowContactSheetLimits,
4198
4524
  tileflowShotFramesSha256,
4199
4525
  tileflowShotReceiptLimits,
4200
4526
  tileflowShotReceiptSchemaVersion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tileflow/capture",
3
- "version": "0.1.0-alpha.38",
3
+ "version": "0.1.0-alpha.40",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Deterministic local visual capture and comparison for Tileflow maps.",
6
6
  "engines": {
@@ -47,8 +47,8 @@
47
47
  "access": "public"
48
48
  },
49
49
  "dependencies": {
50
- "@tileflow/core": ">=0.1.0-alpha.47 <0.1.0-beta.0",
51
- "@tileflow/dev": ">=0.1.0-alpha.42 <0.1.0-beta.0",
50
+ "@tileflow/core": ">=0.1.0-alpha.50 <0.1.0-beta.0",
51
+ "@tileflow/dev": ">=0.1.0-alpha.45 <0.1.0-beta.0",
52
52
  "maplibre-gl": "6.16.0",
53
53
  "pixelmatch": "7.2.0",
54
54
  "playwright": "1.62.1",
@@ -56,23 +56,26 @@
56
56
  },
57
57
  "devDependencies": {
58
58
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
59
- "@types/node": "^26.6.5",
59
+ "@types/node": "^26.6.6",
60
60
  "@types/pngjs": "6.0.5",
61
- "@vitejs/plugin-vue": "^6.0.8",
61
+ "@vitejs/plugin-vue": "^6.0.10",
62
62
  "maplibre-gl-min": "npm:maplibre-gl@6.4.1",
63
63
  "next": "16.4.2",
64
64
  "react": "^19.3.0",
65
65
  "react-dom": "^19.3.0",
66
66
  "svelte": "^5.57.2",
67
+ "terra-draw": "^1.39.0",
68
+ "terra-draw-maplibre-gl-adapter": "^1.5.0",
67
69
  "vite": "^8.3.4",
68
70
  "vue": "^3.5.43",
69
71
  "webpack": "5.108.3",
70
- "@tileflow/maps": "0.1.0-alpha.19",
71
- "@tileflow/next": "0.1.0-alpha.25",
72
- "@tileflow/react": "0.1.0-alpha.37",
73
- "@tileflow/svelte": "0.1.0-alpha.35",
74
- "@tileflow/vite": "0.1.0-alpha.26",
75
- "@tileflow/vue": "0.1.0-alpha.35",
72
+ "@tileflow/dom": "0.1.0-alpha.0",
73
+ "@tileflow/next": "0.1.0-alpha.26",
74
+ "@tileflow/react": "0.1.0-alpha.40",
75
+ "@tileflow/svelte": "0.1.0-alpha.38",
76
+ "@tileflow/maps": "0.1.0-alpha.22",
77
+ "@tileflow/vite": "0.1.0-alpha.27",
78
+ "@tileflow/vue": "0.1.0-alpha.38",
76
79
  "@tileflow/webpack": "0.1.0-alpha.25"
77
80
  },
78
81
  "scripts": {