@tileflow/capture 0.1.0-alpha.28 → 0.1.0-alpha.30
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 +63 -3
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/dist/{chunk-BSTB4FN6.js → chunk-BI5PWIH4.js} +276 -99
- package/dist/index.d.ts +78 -19
- package/dist/index.js +992 -139
- package/dist/receipt-CfZkzyXd.d.ts +222 -0
- package/dist/receipt.d.ts +1 -1
- package/dist/receipt.js +15 -3
- package/docs/receipts-and-comparison.md +9 -2
- package/package.json +16 -16
- package/dist/receipt-Q7wQ68tG.d.ts +0 -160
package/dist/index.js
CHANGED
|
@@ -2,12 +2,18 @@ import {
|
|
|
2
2
|
TileflowCaptureError,
|
|
3
3
|
captureErrorMessage,
|
|
4
4
|
createTileflowCaptureReceipt,
|
|
5
|
+
createTileflowShotReceipt,
|
|
5
6
|
parseTileflowCaptureReceipt,
|
|
7
|
+
parseTileflowShotReceipt,
|
|
6
8
|
serializeTileflowCaptureReceipt,
|
|
9
|
+
serializeTileflowShotReceipt,
|
|
7
10
|
tileflowCaptureReceiptLimits,
|
|
8
11
|
tileflowCaptureReceiptSchemaVersion,
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
tileflowShotReceiptLimits,
|
|
13
|
+
tileflowShotReceiptSchemaVersion,
|
|
14
|
+
validateTileflowCaptureReceipt,
|
|
15
|
+
validateTileflowShotReceipt
|
|
16
|
+
} from "./chunk-BI5PWIH4.js";
|
|
11
17
|
|
|
12
18
|
// src/browser.ts
|
|
13
19
|
import { spawn } from "child_process";
|
|
@@ -296,11 +302,13 @@ import { resolve } from "path";
|
|
|
296
302
|
import {
|
|
297
303
|
compareCodeUnits as compareCodeUnits3,
|
|
298
304
|
normalizeTileflowCaptureScene,
|
|
305
|
+
normalizeTileflowCaptureShot,
|
|
299
306
|
parseResolvedTileflowMap,
|
|
300
307
|
resolveThemeSelection,
|
|
301
|
-
serializeCanonicalJson,
|
|
302
|
-
sha256Hex,
|
|
303
|
-
tileflowCaptureSceneNameSchema
|
|
308
|
+
serializeCanonicalJson as serializeCanonicalJson2,
|
|
309
|
+
sha256Hex as sha256Hex2,
|
|
310
|
+
tileflowCaptureSceneNameSchema,
|
|
311
|
+
tileflowCaptureShotNameSchema
|
|
304
312
|
} from "@tileflow/core";
|
|
305
313
|
import {
|
|
306
314
|
createTileflowBuildArtifacts,
|
|
@@ -332,7 +340,6 @@ var require3 = createRequire2(import.meta.url);
|
|
|
332
340
|
var mapLibreRuntimePaths = Object.freeze({
|
|
333
341
|
css: require3.resolve("maplibre-gl/dist/maplibre-gl.css"),
|
|
334
342
|
main: require3.resolve("maplibre-gl/dist/maplibre-gl.mjs"),
|
|
335
|
-
shared: require3.resolve("maplibre-gl/dist/maplibre-gl-shared.mjs"),
|
|
336
343
|
worker: require3.resolve("maplibre-gl/dist/maplibre-gl-worker.mjs")
|
|
337
344
|
});
|
|
338
345
|
var tileflowCorePackagePath = require3.resolve("@tileflow/core/package.json");
|
|
@@ -342,7 +349,8 @@ var tileflowBrowserModule = `${readFileSync2(
|
|
|
342
349
|
)}
|
|
343
350
|
globalThis.__tileflowRegisterContourProtocol = registerTileflowContourProtocol;
|
|
344
351
|
globalThis.__tileflowRegisterPmtilesProtocol = registerTileflowPmtilesProtocol;
|
|
345
|
-
globalThis.__tileflowAttachAtmosphere = attachTileflowAtmosphere
|
|
352
|
+
globalThis.__tileflowAttachAtmosphere = attachTileflowAtmosphere;
|
|
353
|
+
globalThis.__tileflowCreateThemeController = createTileflowThemeController;`;
|
|
346
354
|
async function captureStandaloneTileflowScene(input) {
|
|
347
355
|
if (input.scene.target.kind !== "map") {
|
|
348
356
|
throw new TileflowCaptureError(
|
|
@@ -350,8 +358,26 @@ async function captureStandaloneTileflowScene(input) {
|
|
|
350
358
|
"Application scenes require an explicit loopback application origin."
|
|
351
359
|
);
|
|
352
360
|
}
|
|
361
|
+
assertValidCaptureStyle(input.style, input.scene.map);
|
|
362
|
+
const { scene, ...pageInput } = input;
|
|
363
|
+
return withStandaloneTileflowPage({ ...pageInput, viewport: scene.viewport }, async (page) => {
|
|
364
|
+
await page.load(getTileflowStyleFontFaces(input.style));
|
|
365
|
+
await page.step("map-idle", "__tileflowCaptureIdle", {
|
|
366
|
+
camera: scene.camera,
|
|
367
|
+
mapEventTimeoutMs: page.mapEventTimeoutMs
|
|
368
|
+
});
|
|
369
|
+
const png = await page.screenshot();
|
|
370
|
+
return {
|
|
371
|
+
height: scene.viewport.height,
|
|
372
|
+
png,
|
|
373
|
+
width: scene.viewport.width,
|
|
374
|
+
...page.result()
|
|
375
|
+
};
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
function assertValidCaptureStyle(style, label) {
|
|
353
379
|
try {
|
|
354
|
-
assertValidTileflowStyle(
|
|
380
|
+
assertValidTileflowStyle(style, label);
|
|
355
381
|
} catch (error) {
|
|
356
382
|
if (error instanceof TileflowStyleValidationError) {
|
|
357
383
|
throw new TileflowCaptureError("STYLE_INVALID", error.message, {
|
|
@@ -364,7 +390,10 @@ async function captureStandaloneTileflowScene(input) {
|
|
|
364
390
|
}
|
|
365
391
|
throw error;
|
|
366
392
|
}
|
|
393
|
+
}
|
|
394
|
+
async function withStandaloneTileflowPage(input, run) {
|
|
367
395
|
const timeoutMs = input.timeoutMs ?? 3e4;
|
|
396
|
+
const mapEventTimeoutMs = Math.max(1, timeoutMs - 1e3);
|
|
368
397
|
const mapLibreRuntimeUrls = createMapLibreRuntimeUrls(
|
|
369
398
|
resolveMapLibreRuntimeDocumentOrigin(input.style)
|
|
370
399
|
);
|
|
@@ -376,6 +405,18 @@ async function captureStandaloneTileflowScene(input) {
|
|
|
376
405
|
termination = "aborted";
|
|
377
406
|
void context?.close().catch(() => void 0);
|
|
378
407
|
};
|
|
408
|
+
const stopTimeout = () => {
|
|
409
|
+
if (timeout) clearTimeout(timeout);
|
|
410
|
+
timeout = void 0;
|
|
411
|
+
};
|
|
412
|
+
const restartTimeout = () => {
|
|
413
|
+
stopTimeout();
|
|
414
|
+
timeout = setTimeout(() => {
|
|
415
|
+
termination = "timeout";
|
|
416
|
+
void context?.close().catch(() => void 0);
|
|
417
|
+
}, timeoutMs);
|
|
418
|
+
timeout.unref?.();
|
|
419
|
+
};
|
|
379
420
|
if (input.signal?.aborted) {
|
|
380
421
|
throw new TileflowCaptureError("ABORTED", "Tileflow capture was aborted.");
|
|
381
422
|
}
|
|
@@ -383,7 +424,7 @@ async function captureStandaloneTileflowScene(input) {
|
|
|
383
424
|
context = await input.browser.newContext({
|
|
384
425
|
acceptDownloads: false,
|
|
385
426
|
colorScheme: "light",
|
|
386
|
-
deviceScaleFactor: input.
|
|
427
|
+
deviceScaleFactor: input.viewport.dpr,
|
|
387
428
|
ignoreHTTPSErrors: false,
|
|
388
429
|
javaScriptEnabled: true,
|
|
389
430
|
locale: "en-US",
|
|
@@ -391,8 +432,8 @@ async function captureStandaloneTileflowScene(input) {
|
|
|
391
432
|
serviceWorkers: "block",
|
|
392
433
|
timezoneId: "UTC",
|
|
393
434
|
viewport: {
|
|
394
|
-
height: input.
|
|
395
|
-
width: input.
|
|
435
|
+
height: input.viewport.height,
|
|
436
|
+
width: input.viewport.width
|
|
396
437
|
}
|
|
397
438
|
});
|
|
398
439
|
input.signal?.addEventListener("abort", onAbort, { once: true });
|
|
@@ -400,11 +441,7 @@ async function captureStandaloneTileflowScene(input) {
|
|
|
400
441
|
onAbort();
|
|
401
442
|
throw new TileflowCaptureError("ABORTED", "Tileflow capture was aborted.");
|
|
402
443
|
}
|
|
403
|
-
|
|
404
|
-
termination = "timeout";
|
|
405
|
-
void context?.close().catch(() => void 0);
|
|
406
|
-
}, timeoutMs);
|
|
407
|
-
timeout.unref?.();
|
|
444
|
+
restartTimeout();
|
|
408
445
|
const assets = new Map(input.assets.map((asset) => [asset.fileName, asset]));
|
|
409
446
|
const frozenResources = new Map(
|
|
410
447
|
input.assets.filter((asset) => asset.url !== void 0).map((asset) => [asset.url, asset])
|
|
@@ -412,7 +449,6 @@ async function captureStandaloneTileflowScene(input) {
|
|
|
412
449
|
const localTilesets = new Map(
|
|
413
450
|
(input.localTilesets ?? []).map((tileset) => [tileset.fileName, tileset])
|
|
414
451
|
);
|
|
415
|
-
const fontFaces = getTileflowStyleFontFaces(input.style);
|
|
416
452
|
const remoteOrigins = /* @__PURE__ */ new Set();
|
|
417
453
|
let lineBackgroundGlyphs;
|
|
418
454
|
let syntheticAssetFailure;
|
|
@@ -439,7 +475,7 @@ async function captureStandaloneTileflowScene(input) {
|
|
|
439
475
|
return;
|
|
440
476
|
}
|
|
441
477
|
if (url.origin === mapLibreRuntimeUrls.origin) {
|
|
442
|
-
const runtime = getMapLibreRuntimeResponse(url.pathname, input.
|
|
478
|
+
const runtime = getMapLibreRuntimeResponse(url.pathname, input.viewport);
|
|
443
479
|
if (runtime) {
|
|
444
480
|
await route.fulfill(runtime);
|
|
445
481
|
return;
|
|
@@ -541,61 +577,67 @@ globalThis.maplibregl = maplibregl;`,
|
|
|
541
577
|
);
|
|
542
578
|
await page.addScriptTag({ content: tileflowBrowserModule, type: "module" });
|
|
543
579
|
await page.addScriptTag({ content: renderMapInPageScript });
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
580
|
+
const step = async (stepPhase, name, value) => {
|
|
581
|
+
phase = stepPhase;
|
|
582
|
+
const result = await page.evaluate(
|
|
583
|
+
({ name: functionName, value: stepInput }) => {
|
|
584
|
+
const renderWindow = window;
|
|
585
|
+
return renderWindow[functionName](stepInput);
|
|
586
|
+
},
|
|
587
|
+
{ name, value }
|
|
588
|
+
);
|
|
589
|
+
assertPhaseSucceeded(result, stepPhase, syntheticAssetFailure, resourceFailures, pageFailed);
|
|
590
|
+
};
|
|
591
|
+
return await run({
|
|
592
|
+
page,
|
|
593
|
+
mapEventTimeoutMs,
|
|
594
|
+
load: (fontFaces) => step("map-load", "__tileflowCaptureLoad", {
|
|
551
595
|
fontFaces,
|
|
552
|
-
mapEventTimeoutMs
|
|
596
|
+
mapEventTimeoutMs,
|
|
553
597
|
style: input.style
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
598
|
+
}),
|
|
599
|
+
step,
|
|
600
|
+
async screenshot() {
|
|
601
|
+
phase = "screenshot";
|
|
602
|
+
let png;
|
|
603
|
+
try {
|
|
604
|
+
png = new Uint8Array(
|
|
605
|
+
await page.locator("#map").screenshot({ animations: "disabled", type: "png" })
|
|
606
|
+
);
|
|
607
|
+
} catch (error) {
|
|
608
|
+
throw new TileflowCaptureError(
|
|
609
|
+
"SCREENSHOT_FAILED",
|
|
610
|
+
"Tileflow could not produce the map PNG.",
|
|
611
|
+
{
|
|
612
|
+
cause: error,
|
|
613
|
+
details: { phase }
|
|
614
|
+
}
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
assertPostScreenshotSucceeded(syntheticAssetFailure, resourceFailures, pageFailed);
|
|
618
|
+
assertPngDimensions(
|
|
619
|
+
png,
|
|
620
|
+
input.viewport.width * input.viewport.dpr,
|
|
621
|
+
input.viewport.height * input.viewport.dpr
|
|
622
|
+
);
|
|
623
|
+
return png;
|
|
562
624
|
},
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
let png;
|
|
571
|
-
try {
|
|
572
|
-
png = new Uint8Array(
|
|
573
|
-
await page.locator("#map").screenshot({ animations: "disabled", type: "png" })
|
|
574
|
-
);
|
|
575
|
-
} catch (error) {
|
|
576
|
-
throw new TileflowCaptureError(
|
|
577
|
-
"SCREENSHOT_FAILED",
|
|
578
|
-
"Tileflow could not produce the map PNG.",
|
|
579
|
-
{
|
|
580
|
-
cause: error,
|
|
581
|
-
details: { phase }
|
|
625
|
+
restartTimeout,
|
|
626
|
+
async outside(work) {
|
|
627
|
+
stopTimeout();
|
|
628
|
+
try {
|
|
629
|
+
return await work();
|
|
630
|
+
} catch (error) {
|
|
631
|
+
throw new CallerFailure(error);
|
|
582
632
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
);
|
|
591
|
-
const warnings = [...remoteOrigins].sort(compareCodeUnits).map((origin) => `Capture requested remote resources from ${origin}.`);
|
|
592
|
-
return {
|
|
593
|
-
height: input.scene.viewport.height,
|
|
594
|
-
networkDependent: remoteOrigins.size > 0,
|
|
595
|
-
png,
|
|
596
|
-
warnings,
|
|
597
|
-
width: input.scene.viewport.width
|
|
598
|
-
};
|
|
633
|
+
},
|
|
634
|
+
result() {
|
|
635
|
+
return {
|
|
636
|
+
networkDependent: remoteOrigins.size > 0,
|
|
637
|
+
warnings: [...remoteOrigins].sort(compareCodeUnits).map((origin) => `Capture requested remote resources from ${origin}.`)
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
});
|
|
599
641
|
} catch (error) {
|
|
600
642
|
if (termination === "aborted") {
|
|
601
643
|
throw new TileflowCaptureError("ABORTED", "Tileflow capture was aborted.", { cause: error });
|
|
@@ -606,6 +648,9 @@ globalThis.maplibregl = maplibregl;`,
|
|
|
606
648
|
details: { phase }
|
|
607
649
|
});
|
|
608
650
|
}
|
|
651
|
+
if (error instanceof CallerFailure) {
|
|
652
|
+
throw error.error;
|
|
653
|
+
}
|
|
609
654
|
if (error instanceof TileflowCaptureError) {
|
|
610
655
|
throw error;
|
|
611
656
|
}
|
|
@@ -626,25 +671,32 @@ globalThis.maplibregl = maplibregl;`,
|
|
|
626
671
|
}
|
|
627
672
|
);
|
|
628
673
|
}
|
|
629
|
-
throw new TileflowCaptureError(
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
674
|
+
throw new TileflowCaptureError(
|
|
675
|
+
"MAP_LOAD_FAILED",
|
|
676
|
+
mapFailureMessage(phase === "theme-blend" || phase === "map-load" ? phase : "map-idle"),
|
|
677
|
+
{
|
|
678
|
+
cause: error,
|
|
679
|
+
details: { phase }
|
|
680
|
+
}
|
|
681
|
+
);
|
|
633
682
|
} finally {
|
|
634
|
-
|
|
635
|
-
clearTimeout(timeout);
|
|
636
|
-
}
|
|
683
|
+
stopTimeout();
|
|
637
684
|
input.signal?.removeEventListener("abort", onAbort);
|
|
638
685
|
await context?.close().catch(() => void 0);
|
|
639
686
|
}
|
|
640
687
|
}
|
|
688
|
+
var CallerFailure = class {
|
|
689
|
+
constructor(error) {
|
|
690
|
+
this.error = error;
|
|
691
|
+
}
|
|
692
|
+
error;
|
|
693
|
+
};
|
|
641
694
|
function createMapLibreRuntimeUrls(origin) {
|
|
642
695
|
return Object.freeze({
|
|
643
696
|
css: `${origin}/__runtime/maplibre-gl.css`,
|
|
644
697
|
document: `${origin}/__runtime/document.html`,
|
|
645
698
|
main: `${origin}/__runtime/maplibre-gl.mjs`,
|
|
646
699
|
origin,
|
|
647
|
-
shared: `${origin}/__runtime/maplibre-gl-shared.mjs`,
|
|
648
700
|
worker: `${origin}/__runtime/maplibre-gl-worker.mjs`
|
|
649
701
|
});
|
|
650
702
|
}
|
|
@@ -702,7 +754,7 @@ function getMapLibreRuntimeResponse(pathname, viewport) {
|
|
|
702
754
|
status: 200
|
|
703
755
|
};
|
|
704
756
|
}
|
|
705
|
-
const asset = pathname === "/__runtime/maplibre-gl.mjs" ? mapLibreRuntimePaths.main : pathname === "/__runtime/maplibre-gl-
|
|
757
|
+
const asset = pathname === "/__runtime/maplibre-gl.mjs" ? mapLibreRuntimePaths.main : pathname === "/__runtime/maplibre-gl-worker.mjs" ? mapLibreRuntimePaths.worker : pathname === "/__runtime/maplibre-gl.css" ? mapLibreRuntimePaths.css : void 0;
|
|
706
758
|
if (!asset) return void 0;
|
|
707
759
|
return {
|
|
708
760
|
body: readFileSync2(asset, "utf8"),
|
|
@@ -873,12 +925,15 @@ function resourceLabel(kind) {
|
|
|
873
925
|
}
|
|
874
926
|
function timeoutMessage(phase) {
|
|
875
927
|
if (phase === "map-load") return "MapLibre did not finish loading before the capture timeout.";
|
|
928
|
+
if (phase === "theme-blend") return "The theme blend was not ready before the capture timeout.";
|
|
876
929
|
if (phase === "map-idle") return "MapLibre did not become idle before the capture timeout.";
|
|
877
930
|
if (phase === "screenshot") return "PNG production exceeded the capture timeout.";
|
|
878
931
|
return "Tileflow capture exceeded its timeout while creating the browser page.";
|
|
879
932
|
}
|
|
880
933
|
function mapFailureMessage(phase) {
|
|
881
|
-
|
|
934
|
+
if (phase === "map-load") return "MapLibre failed while loading the compiled style.";
|
|
935
|
+
if (phase === "theme-blend") return "Tileflow could not prepare the theme blend.";
|
|
936
|
+
return "MapLibre failed while waiting for the committed camera to become idle.";
|
|
882
937
|
}
|
|
883
938
|
function safeMapError(value) {
|
|
884
939
|
return value.replace(/[A-Za-z][A-Za-z0-9+.-]*:\/\/\S+/g, "resource").replace(/[^A-Za-z0-9 _.,:'"@+()[\]/=-]/g, "").trim().slice(0, 200);
|
|
@@ -953,6 +1008,7 @@ window.__tileflowCaptureLoad = async function(input) {
|
|
|
953
1008
|
style: input.style
|
|
954
1009
|
});
|
|
955
1010
|
window.__tileflowMap = map;
|
|
1011
|
+
window.__tileflowCaptureStyle = input.style;
|
|
956
1012
|
window.__tileflowAttachAtmosphere(map);
|
|
957
1013
|
return await waitForMapEvent(map, "load", input.mapEventTimeoutMs);
|
|
958
1014
|
} catch (error) {
|
|
@@ -1024,6 +1080,58 @@ window.__tileflowCaptureIdle = async function(input) {
|
|
|
1024
1080
|
}
|
|
1025
1081
|
};
|
|
1026
1082
|
|
|
1083
|
+
// A shot prepares its theme blend once; each frame then only moves the blend position.
|
|
1084
|
+
window.__tileflowCapturePrepareBlend = async function(input) {
|
|
1085
|
+
const map = window.__tileflowMap;
|
|
1086
|
+
if (!map) return {status: "failed", reason: "error"};
|
|
1087
|
+
|
|
1088
|
+
try {
|
|
1089
|
+
const controller = window.__tileflowCreateThemeController({
|
|
1090
|
+
initial: {style: window.__tileflowCaptureStyle, theme: input.theme},
|
|
1091
|
+
loadFonts: () => Promise.resolve(),
|
|
1092
|
+
map,
|
|
1093
|
+
timeoutMs: input.mapEventTimeoutMs
|
|
1094
|
+
});
|
|
1095
|
+
const themes = input.themes.map((theme) => ({style: theme.style, theme: theme.theme}));
|
|
1096
|
+
const result = await controller.setBlend({position: input.position, themes});
|
|
1097
|
+
if (result.status !== "applied") {
|
|
1098
|
+
controller.dispose();
|
|
1099
|
+
return {status: "failed", reason: "error", message: String(result.error?.message || "")};
|
|
1100
|
+
}
|
|
1101
|
+
window.__tileflowShotBlend = {controller, themes};
|
|
1102
|
+
return {status: "ok"};
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
return {status: "failed", reason: "error", message: String(error?.message || error || "")};
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
window.__tileflowCaptureFrame = async function(input) {
|
|
1109
|
+
const map = window.__tileflowMap;
|
|
1110
|
+
if (!map) return {status: "failed", reason: "error"};
|
|
1111
|
+
|
|
1112
|
+
try {
|
|
1113
|
+
const blend = window.__tileflowShotBlend;
|
|
1114
|
+
if (blend && input.blendPosition !== undefined) {
|
|
1115
|
+
const result = await blend.controller.setBlend({
|
|
1116
|
+
position: input.blendPosition,
|
|
1117
|
+
themes: blend.themes
|
|
1118
|
+
});
|
|
1119
|
+
if (result.status !== "applied") {
|
|
1120
|
+
return {status: "failed", reason: "error", message: String(result.error?.message || "")};
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
applyCamera(map, input.camera);
|
|
1124
|
+
map.triggerRepaint();
|
|
1125
|
+
const result = await waitForMapEvent(map, "idle", input.mapEventTimeoutMs);
|
|
1126
|
+
if (result.status === "failed") return result;
|
|
1127
|
+
await nextFrame();
|
|
1128
|
+
await nextFrame();
|
|
1129
|
+
return {status: "ok"};
|
|
1130
|
+
} catch {
|
|
1131
|
+
return {status: "failed", reason: "error"};
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
|
|
1027
1135
|
function applyCamera(mapInstance, camera) {
|
|
1028
1136
|
if (camera.type === "bounds") {
|
|
1029
1137
|
mapInstance.fitBounds(
|
|
@@ -1511,6 +1619,638 @@ function compareCodeUnits2(left, right) {
|
|
|
1511
1619
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
1512
1620
|
}
|
|
1513
1621
|
|
|
1622
|
+
// src/shot.ts
|
|
1623
|
+
import {
|
|
1624
|
+
serializeCanonicalJson,
|
|
1625
|
+
sha256Hex,
|
|
1626
|
+
tileflowCaptureShotFrames
|
|
1627
|
+
} from "@tileflow/core";
|
|
1628
|
+
import { getTileflowStyleFontFaces as getTileflowStyleFontFaces2 } from "@tileflow/core/runtime";
|
|
1629
|
+
|
|
1630
|
+
// src/shot-encoder.ts
|
|
1631
|
+
var encoderDocumentUrl = `${tileflowSyntheticAssetOrigin}/__tileflow/shot-encoder.html`;
|
|
1632
|
+
var encoderFrameUrl = `${tileflowSyntheticAssetOrigin}/__tileflow/shot-frame.png`;
|
|
1633
|
+
async function openTileflowShotVideoEncoder(input) {
|
|
1634
|
+
const timeoutMs = input.timeoutMs ?? 3e4;
|
|
1635
|
+
let context;
|
|
1636
|
+
const state = {};
|
|
1637
|
+
let pendingPng;
|
|
1638
|
+
const chunks = [];
|
|
1639
|
+
const onAbort = () => {
|
|
1640
|
+
state.termination = "aborted";
|
|
1641
|
+
void context?.close().catch(() => void 0);
|
|
1642
|
+
};
|
|
1643
|
+
const close = async () => {
|
|
1644
|
+
input.signal?.removeEventListener("abort", onAbort);
|
|
1645
|
+
await context?.close().catch(() => void 0);
|
|
1646
|
+
};
|
|
1647
|
+
const run = async (work) => {
|
|
1648
|
+
if (input.signal?.aborted || state.termination === "aborted") {
|
|
1649
|
+
throw new TileflowCaptureError("ABORTED", "Tileflow capture was aborted.");
|
|
1650
|
+
}
|
|
1651
|
+
const timeout = setTimeout(() => {
|
|
1652
|
+
state.termination = "timeout";
|
|
1653
|
+
void context?.close().catch(() => void 0);
|
|
1654
|
+
}, timeoutMs);
|
|
1655
|
+
timeout.unref?.();
|
|
1656
|
+
try {
|
|
1657
|
+
return await work();
|
|
1658
|
+
} catch (error) {
|
|
1659
|
+
const reason = state.termination;
|
|
1660
|
+
if (reason === "aborted") {
|
|
1661
|
+
throw new TileflowCaptureError("ABORTED", "Tileflow capture was aborted.", { cause: error });
|
|
1662
|
+
}
|
|
1663
|
+
if (reason === "timeout") {
|
|
1664
|
+
throw new TileflowCaptureError(
|
|
1665
|
+
"CAPTURE_TIMEOUT",
|
|
1666
|
+
"Video encoding exceeded the capture timeout.",
|
|
1667
|
+
{ cause: error, details: { phase: "video-encode" } }
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
if (error instanceof TileflowCaptureError) throw error;
|
|
1671
|
+
throw encodeFailed(void 0, error);
|
|
1672
|
+
} finally {
|
|
1673
|
+
clearTimeout(timeout);
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
let page;
|
|
1677
|
+
let codec;
|
|
1678
|
+
try {
|
|
1679
|
+
input.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1680
|
+
const opened = await run(async () => {
|
|
1681
|
+
context = await input.browser.newContext({
|
|
1682
|
+
acceptDownloads: false,
|
|
1683
|
+
javaScriptEnabled: true,
|
|
1684
|
+
serviceWorkers: "block",
|
|
1685
|
+
viewport: { height: 64, width: 64 }
|
|
1686
|
+
});
|
|
1687
|
+
await context.route("**/*", async (route) => {
|
|
1688
|
+
const url = route.request().url();
|
|
1689
|
+
if (url === encoderDocumentUrl) {
|
|
1690
|
+
await route.fulfill({
|
|
1691
|
+
body: '<!doctype html><html lang="en"><head><meta charset="utf-8" /></head><body></body></html>',
|
|
1692
|
+
contentType: "text/html; charset=utf-8",
|
|
1693
|
+
status: 200
|
|
1694
|
+
});
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
if (url === encoderFrameUrl && pendingPng) {
|
|
1698
|
+
await route.fulfill({
|
|
1699
|
+
body: Buffer.from(pendingPng),
|
|
1700
|
+
contentType: "image/png",
|
|
1701
|
+
status: 200
|
|
1702
|
+
});
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
await route.abort("blockedbyclient");
|
|
1706
|
+
});
|
|
1707
|
+
await context.routeWebSocket(
|
|
1708
|
+
/.*/,
|
|
1709
|
+
(socket) => socket.close({ code: 1008, reason: "WebSockets are disabled during capture" })
|
|
1710
|
+
);
|
|
1711
|
+
const encoderPage = await context.newPage();
|
|
1712
|
+
await encoderPage.goto(encoderDocumentUrl, { waitUntil: "domcontentloaded" });
|
|
1713
|
+
await encoderPage.addScriptTag({ content: encoderPageScript });
|
|
1714
|
+
const result = await encoderPage.evaluate(
|
|
1715
|
+
(config) => window.__tileflowOpenShotEncoder(config),
|
|
1716
|
+
{
|
|
1717
|
+
bitrate: shotBitrate(input.width, input.height, input.fps),
|
|
1718
|
+
fps: input.fps,
|
|
1719
|
+
frameUrl: encoderFrameUrl,
|
|
1720
|
+
height: input.height,
|
|
1721
|
+
width: input.width
|
|
1722
|
+
}
|
|
1723
|
+
);
|
|
1724
|
+
return { page: encoderPage, result };
|
|
1725
|
+
});
|
|
1726
|
+
page = opened.page;
|
|
1727
|
+
if (opened.result.status === "unsupported" || !opened.result.codec) {
|
|
1728
|
+
throw new TileflowCaptureError(
|
|
1729
|
+
"VIDEO_ENCODER_UNAVAILABLE",
|
|
1730
|
+
`The capture browser cannot encode ${input.width}x${input.height} VP9 or VP8 video. Capture the shot with format "frames" instead.`,
|
|
1731
|
+
{ details: { phase: "video-encode" } }
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
if (opened.result.status !== "ok") throw encodeFailed(opened.result.message);
|
|
1735
|
+
codec = opened.result.codec;
|
|
1736
|
+
} catch (error) {
|
|
1737
|
+
await close();
|
|
1738
|
+
throw error;
|
|
1739
|
+
}
|
|
1740
|
+
const collect = (result) => {
|
|
1741
|
+
if (result.status !== "ok") throw encodeFailed(result.message);
|
|
1742
|
+
for (const chunk of result.chunks) {
|
|
1743
|
+
chunks.push({
|
|
1744
|
+
data: new Uint8Array(Buffer.from(chunk.data, "base64")),
|
|
1745
|
+
key: chunk.key,
|
|
1746
|
+
timestamp: chunk.timestamp
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
};
|
|
1750
|
+
return {
|
|
1751
|
+
codec,
|
|
1752
|
+
async encode(index, png) {
|
|
1753
|
+
await run(async () => {
|
|
1754
|
+
pendingPng = png;
|
|
1755
|
+
try {
|
|
1756
|
+
collect(
|
|
1757
|
+
await page.evaluate(
|
|
1758
|
+
(frame) => window.__tileflowEncodeShotFrame(frame),
|
|
1759
|
+
index
|
|
1760
|
+
)
|
|
1761
|
+
);
|
|
1762
|
+
} finally {
|
|
1763
|
+
pendingPng = void 0;
|
|
1764
|
+
}
|
|
1765
|
+
});
|
|
1766
|
+
},
|
|
1767
|
+
async finish() {
|
|
1768
|
+
await run(
|
|
1769
|
+
async () => collect(
|
|
1770
|
+
await page.evaluate(
|
|
1771
|
+
() => window.__tileflowFinishShotEncoder()
|
|
1772
|
+
)
|
|
1773
|
+
)
|
|
1774
|
+
);
|
|
1775
|
+
return [...chunks];
|
|
1776
|
+
},
|
|
1777
|
+
close
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
function shotBitrate(width, height, fps) {
|
|
1781
|
+
return Math.min(24e6, Math.max(25e4, Math.round(width * height * fps * 0.2)));
|
|
1782
|
+
}
|
|
1783
|
+
function encodeFailed(message, cause) {
|
|
1784
|
+
const detail = message ? ` ${message.replace(/[^A-Za-z0-9 _.,:'"()[\]/=-]/g, "").trim().slice(0, 200)}` : "";
|
|
1785
|
+
return new TileflowCaptureError("VIDEO_ENCODE_FAILED", `Video encoding failed.${detail}`, {
|
|
1786
|
+
...cause === void 0 ? {} : { cause },
|
|
1787
|
+
details: { phase: "video-encode" }
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
var encoderPageScript = String.raw`
|
|
1791
|
+
let encoder;
|
|
1792
|
+
let encoderError;
|
|
1793
|
+
let frameDuration = 0;
|
|
1794
|
+
let keyInterval = 1;
|
|
1795
|
+
let frameUrl = "";
|
|
1796
|
+
const pending = [];
|
|
1797
|
+
|
|
1798
|
+
window.__tileflowOpenShotEncoder = async function(input) {
|
|
1799
|
+
if (typeof VideoEncoder !== "function" || typeof VideoFrame !== "function") {
|
|
1800
|
+
return {status: "unsupported"};
|
|
1801
|
+
}
|
|
1802
|
+
frameDuration = 1e6 / input.fps;
|
|
1803
|
+
keyInterval = input.fps;
|
|
1804
|
+
frameUrl = input.frameUrl;
|
|
1805
|
+
// VP9 profile 0, 8-bit; VP8 where VP9 is unavailable.
|
|
1806
|
+
for (const [codec, codecString] of [["vp9", "vp09.00.10.08"], ["vp8", "vp8"]]) {
|
|
1807
|
+
const config = {
|
|
1808
|
+
bitrate: input.bitrate,
|
|
1809
|
+
codec: codecString,
|
|
1810
|
+
framerate: input.fps,
|
|
1811
|
+
height: input.height,
|
|
1812
|
+
latencyMode: "quality",
|
|
1813
|
+
width: input.width
|
|
1814
|
+
};
|
|
1815
|
+
let support;
|
|
1816
|
+
try {
|
|
1817
|
+
support = await VideoEncoder.isConfigSupported(config);
|
|
1818
|
+
} catch {
|
|
1819
|
+
continue;
|
|
1820
|
+
}
|
|
1821
|
+
if (!support.supported) continue;
|
|
1822
|
+
try {
|
|
1823
|
+
encoder = new VideoEncoder({
|
|
1824
|
+
error: (error) => {
|
|
1825
|
+
encoderError = String(error?.message || error || "encoder error");
|
|
1826
|
+
},
|
|
1827
|
+
output: (chunk) => {
|
|
1828
|
+
const data = new Uint8Array(chunk.byteLength);
|
|
1829
|
+
chunk.copyTo(data);
|
|
1830
|
+
pending.push({data: toBase64(data), key: chunk.type === "key", timestamp: chunk.timestamp});
|
|
1831
|
+
}
|
|
1832
|
+
});
|
|
1833
|
+
encoder.configure(config);
|
|
1834
|
+
return {codec, status: "ok"};
|
|
1835
|
+
} catch (error) {
|
|
1836
|
+
return {message: String(error?.message || error || ""), status: "failed"};
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
return {status: "unsupported"};
|
|
1840
|
+
};
|
|
1841
|
+
|
|
1842
|
+
window.__tileflowEncodeShotFrame = async function(index) {
|
|
1843
|
+
try {
|
|
1844
|
+
if (encoderError) return {message: encoderError, status: "failed"};
|
|
1845
|
+
const response = await fetch(frameUrl, {cache: "no-store"});
|
|
1846
|
+
if (!response.ok) return {message: "frame unavailable", status: "failed"};
|
|
1847
|
+
const bitmap = await createImageBitmap(await response.blob(), {
|
|
1848
|
+
colorSpaceConversion: "none",
|
|
1849
|
+
premultiplyAlpha: "none"
|
|
1850
|
+
});
|
|
1851
|
+
const frame = new VideoFrame(bitmap, {
|
|
1852
|
+
duration: Math.round(frameDuration),
|
|
1853
|
+
timestamp: Math.round(index * frameDuration)
|
|
1854
|
+
});
|
|
1855
|
+
bitmap.close();
|
|
1856
|
+
try {
|
|
1857
|
+
encoder.encode(frame, {keyFrame: index % keyInterval === 0});
|
|
1858
|
+
} finally {
|
|
1859
|
+
frame.close();
|
|
1860
|
+
}
|
|
1861
|
+
while (encoder.encodeQueueSize > 2 && !encoderError) {
|
|
1862
|
+
await new Promise((resolve) => encoder.addEventListener("dequeue", resolve, {once: true}));
|
|
1863
|
+
}
|
|
1864
|
+
if (encoderError) return {message: encoderError, status: "failed"};
|
|
1865
|
+
return {chunks: pending.splice(0), status: "ok"};
|
|
1866
|
+
} catch (error) {
|
|
1867
|
+
return {message: String(error?.message || error || ""), status: "failed"};
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
|
|
1871
|
+
window.__tileflowFinishShotEncoder = async function() {
|
|
1872
|
+
try {
|
|
1873
|
+
await encoder.flush();
|
|
1874
|
+
encoder.close();
|
|
1875
|
+
if (encoderError) return {message: encoderError, status: "failed"};
|
|
1876
|
+
return {chunks: pending.splice(0), status: "ok"};
|
|
1877
|
+
} catch (error) {
|
|
1878
|
+
return {message: String(encoderError || error?.message || error || ""), status: "failed"};
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
|
|
1882
|
+
function toBase64(bytes) {
|
|
1883
|
+
let text = "";
|
|
1884
|
+
for (let offset = 0; offset < bytes.length; offset += 0x8000) {
|
|
1885
|
+
text += String.fromCharCode.apply(null, bytes.subarray(offset, offset + 0x8000));
|
|
1886
|
+
}
|
|
1887
|
+
return btoa(text);
|
|
1888
|
+
}
|
|
1889
|
+
`;
|
|
1890
|
+
|
|
1891
|
+
// src/webm.ts
|
|
1892
|
+
var tileflowWebmElementIds = Object.freeze({
|
|
1893
|
+
ebml: 440786851,
|
|
1894
|
+
ebmlVersion: 17030,
|
|
1895
|
+
ebmlReadVersion: 17143,
|
|
1896
|
+
ebmlMaxIdLength: 17138,
|
|
1897
|
+
ebmlMaxSizeLength: 17139,
|
|
1898
|
+
docType: 17026,
|
|
1899
|
+
docTypeVersion: 17031,
|
|
1900
|
+
docTypeReadVersion: 17029,
|
|
1901
|
+
segment: 408125543,
|
|
1902
|
+
seekHead: 290298740,
|
|
1903
|
+
seek: 19899,
|
|
1904
|
+
seekId: 21419,
|
|
1905
|
+
seekPosition: 21420,
|
|
1906
|
+
info: 357149030,
|
|
1907
|
+
timestampScale: 2807729,
|
|
1908
|
+
duration: 17545,
|
|
1909
|
+
muxingApp: 19840,
|
|
1910
|
+
writingApp: 22337,
|
|
1911
|
+
tracks: 374648427,
|
|
1912
|
+
trackEntry: 174,
|
|
1913
|
+
trackNumber: 215,
|
|
1914
|
+
trackUid: 29637,
|
|
1915
|
+
trackType: 131,
|
|
1916
|
+
flagLacing: 156,
|
|
1917
|
+
defaultDuration: 2352003,
|
|
1918
|
+
codecId: 134,
|
|
1919
|
+
video: 224,
|
|
1920
|
+
pixelWidth: 176,
|
|
1921
|
+
pixelHeight: 186,
|
|
1922
|
+
cluster: 524531317,
|
|
1923
|
+
timestamp: 231,
|
|
1924
|
+
simpleBlock: 163,
|
|
1925
|
+
cues: 475249515,
|
|
1926
|
+
cuePoint: 187,
|
|
1927
|
+
cueTime: 179,
|
|
1928
|
+
cueTrackPositions: 183,
|
|
1929
|
+
cueTrack: 247,
|
|
1930
|
+
cueClusterPosition: 241
|
|
1931
|
+
});
|
|
1932
|
+
var id = tileflowWebmElementIds;
|
|
1933
|
+
var maximumRelativeTimestamp = 32767;
|
|
1934
|
+
function writeTileflowWebm(input) {
|
|
1935
|
+
if (input.chunks.length === 0 || !input.chunks[0].key) {
|
|
1936
|
+
throw new RangeError("A WebM video must start with a key frame.");
|
|
1937
|
+
}
|
|
1938
|
+
const header = element(id.ebml, [
|
|
1939
|
+
unsignedElement(id.ebmlVersion, 1),
|
|
1940
|
+
unsignedElement(id.ebmlReadVersion, 1),
|
|
1941
|
+
unsignedElement(id.ebmlMaxIdLength, 4),
|
|
1942
|
+
unsignedElement(id.ebmlMaxSizeLength, 8),
|
|
1943
|
+
stringElement(id.docType, "webm"),
|
|
1944
|
+
unsignedElement(id.docTypeVersion, 4),
|
|
1945
|
+
unsignedElement(id.docTypeReadVersion, 2)
|
|
1946
|
+
]);
|
|
1947
|
+
const info = element(id.info, [
|
|
1948
|
+
unsignedElement(id.timestampScale, 1e6),
|
|
1949
|
+
floatElement(id.duration, input.durationMs),
|
|
1950
|
+
stringElement(id.muxingApp, "Tileflow"),
|
|
1951
|
+
stringElement(id.writingApp, "Tileflow")
|
|
1952
|
+
]);
|
|
1953
|
+
const tracks = element(id.tracks, [
|
|
1954
|
+
element(id.trackEntry, [
|
|
1955
|
+
unsignedElement(id.trackNumber, 1),
|
|
1956
|
+
unsignedElement(id.trackUid, 1),
|
|
1957
|
+
unsignedElement(id.trackType, 1),
|
|
1958
|
+
unsignedElement(id.flagLacing, 0),
|
|
1959
|
+
unsignedElement(id.defaultDuration, Math.round(1e9 / input.fps)),
|
|
1960
|
+
stringElement(id.codecId, input.codec === "vp9" ? "V_VP9" : "V_VP8"),
|
|
1961
|
+
element(id.video, [
|
|
1962
|
+
unsignedElement(id.pixelWidth, input.width),
|
|
1963
|
+
unsignedElement(id.pixelHeight, input.height)
|
|
1964
|
+
])
|
|
1965
|
+
])
|
|
1966
|
+
]);
|
|
1967
|
+
const clusters = [];
|
|
1968
|
+
let current;
|
|
1969
|
+
const closeCluster = () => {
|
|
1970
|
+
if (!current) return;
|
|
1971
|
+
clusters.push({
|
|
1972
|
+
bytes: element(id.cluster, [
|
|
1973
|
+
unsignedElement(id.timestamp, current.timestamp),
|
|
1974
|
+
...current.blocks
|
|
1975
|
+
]),
|
|
1976
|
+
timestamp: current.timestamp
|
|
1977
|
+
});
|
|
1978
|
+
current = void 0;
|
|
1979
|
+
};
|
|
1980
|
+
for (const chunk of input.chunks) {
|
|
1981
|
+
const timestamp = Math.round(chunk.timestamp / 1e3);
|
|
1982
|
+
if (!current || chunk.key || timestamp - current.timestamp > maximumRelativeTimestamp) {
|
|
1983
|
+
closeCluster();
|
|
1984
|
+
current = { blocks: [], timestamp };
|
|
1985
|
+
}
|
|
1986
|
+
current.blocks.push(simpleBlock(timestamp - current.timestamp, chunk.key, chunk.data));
|
|
1987
|
+
}
|
|
1988
|
+
closeCluster();
|
|
1989
|
+
const seekHeadSize = seekHead([0, 0, 0]).byteLength;
|
|
1990
|
+
const infoPosition = seekHeadSize;
|
|
1991
|
+
const tracksPosition = infoPosition + info.byteLength;
|
|
1992
|
+
let position = tracksPosition + tracks.byteLength;
|
|
1993
|
+
const cuePoints = [];
|
|
1994
|
+
for (const cluster of clusters) {
|
|
1995
|
+
cuePoints.push(
|
|
1996
|
+
element(id.cuePoint, [
|
|
1997
|
+
unsignedElement(id.cueTime, cluster.timestamp),
|
|
1998
|
+
element(id.cueTrackPositions, [
|
|
1999
|
+
unsignedElement(id.cueTrack, 1),
|
|
2000
|
+
unsignedElement(id.cueClusterPosition, position)
|
|
2001
|
+
])
|
|
2002
|
+
])
|
|
2003
|
+
);
|
|
2004
|
+
position += cluster.bytes.byteLength;
|
|
2005
|
+
}
|
|
2006
|
+
const cuesPosition = position;
|
|
2007
|
+
const segment = element(id.segment, [
|
|
2008
|
+
seekHead([infoPosition, tracksPosition, cuesPosition]),
|
|
2009
|
+
info,
|
|
2010
|
+
tracks,
|
|
2011
|
+
...clusters.map((cluster) => cluster.bytes),
|
|
2012
|
+
element(id.cues, cuePoints)
|
|
2013
|
+
]);
|
|
2014
|
+
return concat([header, segment]);
|
|
2015
|
+
}
|
|
2016
|
+
function seekHead([info, tracks, cues]) {
|
|
2017
|
+
const seek = (target, offset) => element(id.seek, [
|
|
2018
|
+
binaryElement(id.seekId, elementIdBytes(target)),
|
|
2019
|
+
element(id.seekPosition, [fixedUnsigned(offset, 8)])
|
|
2020
|
+
]);
|
|
2021
|
+
return element(id.seekHead, [seek(id.info, info), seek(id.tracks, tracks), seek(id.cues, cues)]);
|
|
2022
|
+
}
|
|
2023
|
+
function simpleBlock(relativeTimestamp, key, data) {
|
|
2024
|
+
const header = new Uint8Array(4);
|
|
2025
|
+
header[0] = 129;
|
|
2026
|
+
new DataView(header.buffer).setInt16(1, relativeTimestamp);
|
|
2027
|
+
header[3] = key ? 128 : 0;
|
|
2028
|
+
return binaryElement(id.simpleBlock, concat([header, data]));
|
|
2029
|
+
}
|
|
2030
|
+
function element(elementId, children) {
|
|
2031
|
+
return binaryElement(elementId, concat(children));
|
|
2032
|
+
}
|
|
2033
|
+
function binaryElement(elementId, data) {
|
|
2034
|
+
return concat([elementIdBytes(elementId), sizeBytes(data.byteLength), data]);
|
|
2035
|
+
}
|
|
2036
|
+
function unsignedElement(elementId, value) {
|
|
2037
|
+
return binaryElement(elementId, minimalUnsigned(value));
|
|
2038
|
+
}
|
|
2039
|
+
function stringElement(elementId, value) {
|
|
2040
|
+
return binaryElement(elementId, new TextEncoder().encode(value));
|
|
2041
|
+
}
|
|
2042
|
+
function floatElement(elementId, value) {
|
|
2043
|
+
const bytes = new Uint8Array(8);
|
|
2044
|
+
new DataView(bytes.buffer).setFloat64(0, value);
|
|
2045
|
+
return binaryElement(elementId, bytes);
|
|
2046
|
+
}
|
|
2047
|
+
function elementIdBytes(elementId) {
|
|
2048
|
+
const length = elementId >= 16777216 ? 4 : elementId >= 65536 ? 3 : elementId >= 256 ? 2 : 1;
|
|
2049
|
+
return fixedUnsigned(elementId, length);
|
|
2050
|
+
}
|
|
2051
|
+
function sizeBytes(size) {
|
|
2052
|
+
if (!Number.isSafeInteger(size) || size < 0) throw new RangeError("Invalid EBML element size.");
|
|
2053
|
+
let length = 1;
|
|
2054
|
+
while (length < 8 && size >= 2 ** (7 * length) - 1) length += 1;
|
|
2055
|
+
if (size >= 2 ** (7 * length) - 1) throw new RangeError("EBML element is too large.");
|
|
2056
|
+
const bytes = fixedUnsigned(size, length);
|
|
2057
|
+
bytes[0] = (bytes[0] ?? 0) | 1 << 8 - length;
|
|
2058
|
+
return bytes;
|
|
2059
|
+
}
|
|
2060
|
+
function minimalUnsigned(value) {
|
|
2061
|
+
let length = 1;
|
|
2062
|
+
while (length < 8 && value >= 2 ** (8 * length)) length += 1;
|
|
2063
|
+
return fixedUnsigned(value, length);
|
|
2064
|
+
}
|
|
2065
|
+
function fixedUnsigned(value, length) {
|
|
2066
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new RangeError("Invalid EBML integer.");
|
|
2067
|
+
const bytes = new Uint8Array(length);
|
|
2068
|
+
let remaining = value;
|
|
2069
|
+
for (let index = length - 1; index >= 0; index -= 1) {
|
|
2070
|
+
bytes[index] = remaining % 256;
|
|
2071
|
+
remaining = Math.floor(remaining / 256);
|
|
2072
|
+
}
|
|
2073
|
+
return bytes;
|
|
2074
|
+
}
|
|
2075
|
+
function concat(parts) {
|
|
2076
|
+
const bytes = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
|
|
2077
|
+
let offset = 0;
|
|
2078
|
+
for (const part of parts) {
|
|
2079
|
+
bytes.set(part, offset);
|
|
2080
|
+
offset += part.byteLength;
|
|
2081
|
+
}
|
|
2082
|
+
return bytes;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
// src/shot.ts
|
|
2086
|
+
async function renderTileflowShot(input) {
|
|
2087
|
+
const { shot } = input;
|
|
2088
|
+
const width = shot.viewport.width * shot.viewport.dpr;
|
|
2089
|
+
const height = shot.viewport.height * shot.viewport.dpr;
|
|
2090
|
+
let encoder;
|
|
2091
|
+
try {
|
|
2092
|
+
if (input.format === "webm") {
|
|
2093
|
+
encoder = await openTileflowShotVideoEncoder({
|
|
2094
|
+
browser: input.browser,
|
|
2095
|
+
fps: shot.fps,
|
|
2096
|
+
height,
|
|
2097
|
+
...input.signal ? { signal: input.signal } : {},
|
|
2098
|
+
...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs },
|
|
2099
|
+
width
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
const frameSha256s = [];
|
|
2103
|
+
const rendered = await captureStandaloneTileflowShot({
|
|
2104
|
+
...input,
|
|
2105
|
+
onFrame: async ({ index, png }) => {
|
|
2106
|
+
const sha2562 = await sha256Hex(png);
|
|
2107
|
+
frameSha256s.push(sha2562);
|
|
2108
|
+
await input.onFrame?.({ index, png, sha256: sha2562, shot: input.name });
|
|
2109
|
+
await encoder?.encode(index, png);
|
|
2110
|
+
}
|
|
2111
|
+
});
|
|
2112
|
+
const durationMs = shot.keyframes.at(-1).at;
|
|
2113
|
+
const framesSha256 = await tileflowShotFramesSha256(frameSha256s);
|
|
2114
|
+
const video = encoder ? writeTileflowWebm({
|
|
2115
|
+
chunks: await encoder.finish(),
|
|
2116
|
+
codec: encoder.codec,
|
|
2117
|
+
durationMs: rendered.frameCount * 1e3 / shot.fps,
|
|
2118
|
+
fps: shot.fps,
|
|
2119
|
+
height,
|
|
2120
|
+
width
|
|
2121
|
+
}) : void 0;
|
|
2122
|
+
const blendThemes = shot.keyframes[0]?.blend?.themes;
|
|
2123
|
+
const styleHashes = /* @__PURE__ */ new Map();
|
|
2124
|
+
for (const theme of /* @__PURE__ */ new Set([shot.theme, ...blendThemes ?? []])) {
|
|
2125
|
+
styleHashes.set(theme, await sha256Hex(serializeCanonicalJson(input.styles[theme])));
|
|
2126
|
+
}
|
|
2127
|
+
const [sha256, shotSha256] = await Promise.all([
|
|
2128
|
+
video ? sha256Hex(video) : Promise.resolve(framesSha256),
|
|
2129
|
+
sha256Hex(serializeCanonicalJson(shot))
|
|
2130
|
+
]);
|
|
2131
|
+
const styleSha256 = styleHashes.get(shot.theme);
|
|
2132
|
+
const receipt = createTileflowShotReceipt({
|
|
2133
|
+
...blendThemes ? { blend: blendThemes.map((name) => ({ name, styleSha256: styleHashes.get(name) })) } : {},
|
|
2134
|
+
data: input.data,
|
|
2135
|
+
dpr: shot.viewport.dpr,
|
|
2136
|
+
durationMs,
|
|
2137
|
+
fps: shot.fps,
|
|
2138
|
+
frameCount: rendered.frameCount,
|
|
2139
|
+
framesSha256,
|
|
2140
|
+
height: rendered.height,
|
|
2141
|
+
map: shot.map,
|
|
2142
|
+
networkDependent: rendered.networkDependent,
|
|
2143
|
+
output: encoder ? { codec: encoder.codec, kind: "webm", sha256 } : { kind: "frames", sha256 },
|
|
2144
|
+
renderer: input.renderer,
|
|
2145
|
+
shot: input.name,
|
|
2146
|
+
shotSha256,
|
|
2147
|
+
styleSha256,
|
|
2148
|
+
theme: shot.theme,
|
|
2149
|
+
width: rendered.width
|
|
2150
|
+
});
|
|
2151
|
+
return {
|
|
2152
|
+
shot: input.name,
|
|
2153
|
+
map: shot.map,
|
|
2154
|
+
theme: shot.theme,
|
|
2155
|
+
format: input.format,
|
|
2156
|
+
...video ? { video } : {},
|
|
2157
|
+
...encoder ? { codec: encoder.codec } : {},
|
|
2158
|
+
sha256,
|
|
2159
|
+
framesSha256,
|
|
2160
|
+
frameSha256s,
|
|
2161
|
+
shotSha256,
|
|
2162
|
+
styleSha256,
|
|
2163
|
+
fps: shot.fps,
|
|
2164
|
+
frameCount: rendered.frameCount,
|
|
2165
|
+
durationMs,
|
|
2166
|
+
width: rendered.width,
|
|
2167
|
+
height: rendered.height,
|
|
2168
|
+
dpr: shot.viewport.dpr,
|
|
2169
|
+
networkDependent: rendered.networkDependent,
|
|
2170
|
+
renderer: input.renderer,
|
|
2171
|
+
receipt,
|
|
2172
|
+
warnings: rendered.warnings
|
|
2173
|
+
};
|
|
2174
|
+
} finally {
|
|
2175
|
+
await encoder?.close();
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
function tileflowShotFramesSha256(frameSha256s) {
|
|
2179
|
+
return sha256Hex(frameSha256s.map((sha256) => `${sha256}
|
|
2180
|
+
`).join(""));
|
|
2181
|
+
}
|
|
2182
|
+
async function captureStandaloneTileflowShot(input) {
|
|
2183
|
+
const { shot } = input;
|
|
2184
|
+
const frames = tileflowCaptureShotFrames(shot);
|
|
2185
|
+
const blendThemes = shot.keyframes[0]?.blend?.themes;
|
|
2186
|
+
const themes = [.../* @__PURE__ */ new Set([shot.theme, ...blendThemes ?? []])];
|
|
2187
|
+
const styles = themes.map((theme) => {
|
|
2188
|
+
const style = Object.hasOwn(input.styles, theme) ? input.styles[theme] : void 0;
|
|
2189
|
+
if (!style) {
|
|
2190
|
+
throw new TileflowCaptureError(
|
|
2191
|
+
"SHOT_NOT_FOUND",
|
|
2192
|
+
`Shot theme "${theme}" has no compiled style on map "${shot.map}".`
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
assertValidCaptureStyle(style, `${shot.map}/${theme}`);
|
|
2196
|
+
return [theme, pageStyle(style)];
|
|
2197
|
+
});
|
|
2198
|
+
const pageStyles = Object.fromEntries(styles);
|
|
2199
|
+
return withStandaloneTileflowPage(
|
|
2200
|
+
{
|
|
2201
|
+
assets: input.assets,
|
|
2202
|
+
browser: input.browser,
|
|
2203
|
+
...input.glyphCacheDirectory ? { glyphCacheDirectory: input.glyphCacheDirectory } : {},
|
|
2204
|
+
...input.lineBackgroundGlyphs ? { lineBackgroundGlyphs: input.lineBackgroundGlyphs } : {},
|
|
2205
|
+
...input.localTilesets ? { localTilesets: input.localTilesets } : {},
|
|
2206
|
+
...input.signal ? { signal: input.signal } : {},
|
|
2207
|
+
style: pageStyles[shot.theme],
|
|
2208
|
+
...input.timeoutMs === void 0 ? {} : { timeoutMs: input.timeoutMs },
|
|
2209
|
+
viewport: shot.viewport
|
|
2210
|
+
},
|
|
2211
|
+
async (page) => {
|
|
2212
|
+
await page.load(uniqueFontFaces(styles.map(([, style]) => style)));
|
|
2213
|
+
if (blendThemes) {
|
|
2214
|
+
page.restartTimeout();
|
|
2215
|
+
await page.step("theme-blend", "__tileflowCapturePrepareBlend", {
|
|
2216
|
+
mapEventTimeoutMs: page.mapEventTimeoutMs,
|
|
2217
|
+
position: frames[0]?.blendPosition ?? 0,
|
|
2218
|
+
theme: shot.theme,
|
|
2219
|
+
themes: blendThemes.map((theme) => ({ style: pageStyles[theme], theme }))
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
for (const frame of frames) {
|
|
2223
|
+
page.restartTimeout();
|
|
2224
|
+
await page.step("map-idle", "__tileflowCaptureFrame", {
|
|
2225
|
+
blendPosition: frame.blendPosition,
|
|
2226
|
+
camera: frame.camera,
|
|
2227
|
+
mapEventTimeoutMs: page.mapEventTimeoutMs
|
|
2228
|
+
});
|
|
2229
|
+
const png = await page.screenshot();
|
|
2230
|
+
await page.outside(async () => input.onFrame({ index: frame.index, png }));
|
|
2231
|
+
}
|
|
2232
|
+
return {
|
|
2233
|
+
frameCount: frames.length,
|
|
2234
|
+
height: shot.viewport.height,
|
|
2235
|
+
width: shot.viewport.width,
|
|
2236
|
+
...page.result()
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
);
|
|
2240
|
+
}
|
|
2241
|
+
function pageStyle(style) {
|
|
2242
|
+
return { ...style, transition: { delay: 0, duration: 0 } };
|
|
2243
|
+
}
|
|
2244
|
+
function uniqueFontFaces(styles) {
|
|
2245
|
+
const faces = /* @__PURE__ */ new Map();
|
|
2246
|
+
for (const style of styles) {
|
|
2247
|
+
for (const face of getTileflowStyleFontFaces2(style) ?? []) {
|
|
2248
|
+
faces.set(JSON.stringify(face), face);
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
return [...faces.values()];
|
|
2252
|
+
}
|
|
2253
|
+
|
|
1514
2254
|
// src/world.ts
|
|
1515
2255
|
import { createHash } from "crypto";
|
|
1516
2256
|
import {
|
|
@@ -1534,13 +2274,12 @@ var TileflowCaptureWorldSession = class {
|
|
|
1534
2274
|
const source = requirePrimaryVectorSource(style);
|
|
1535
2275
|
const lookupUrl = worldTileJsonLookupUrl(source, metadata);
|
|
1536
2276
|
const resolution = await this.#resolveOne(lookupUrl, signal);
|
|
1537
|
-
const semantics = validateSemantics(metadata.semantics);
|
|
1538
2277
|
const data = {
|
|
1539
2278
|
...resolution.identity,
|
|
1540
2279
|
kind: "tileflow-world",
|
|
1541
2280
|
schema: requireLiteral(metadata.schema, "openmaptiles", "schema"),
|
|
1542
2281
|
schemaVersion: requirePositiveInteger(metadata.schemaVersion, "schemaVersion"),
|
|
1543
|
-
|
|
2282
|
+
semantics: validateSemantics(metadata.semantics),
|
|
1544
2283
|
sourceId: requireLiteral(metadata.sourceId, "tileflow", "sourceId")
|
|
1545
2284
|
};
|
|
1546
2285
|
const { tiles: _mutableTiles, url: _mutableUrl, ...sourceWithoutSelector } = source;
|
|
@@ -1726,7 +2465,6 @@ function validateExactTileTemplate(value, releaseId, descriptorSha256) {
|
|
|
1726
2465
|
return value;
|
|
1727
2466
|
}
|
|
1728
2467
|
function validateSemantics(value) {
|
|
1729
|
-
if (value === void 0) return void 0;
|
|
1730
2468
|
const semantics = requireRecord(value, "data semantics");
|
|
1731
2469
|
requireExactKeys(semantics, ["parkLayer"]);
|
|
1732
2470
|
if (semantics.parkLayer !== "mixed" && semantics.parkLayer !== "protected-only") {
|
|
@@ -1789,6 +2527,18 @@ async function captureTileflowScenes(options) {
|
|
|
1789
2527
|
await session.close();
|
|
1790
2528
|
}
|
|
1791
2529
|
}
|
|
2530
|
+
async function captureTileflowShot(options) {
|
|
2531
|
+
const { format, onFrame, shot, ...sessionOptions } = options;
|
|
2532
|
+
const session = createTileflowCaptureSession(sessionOptions);
|
|
2533
|
+
try {
|
|
2534
|
+
return await session.captureShot(shot, {
|
|
2535
|
+
...format ? { format } : {},
|
|
2536
|
+
...onFrame ? { onFrame } : {}
|
|
2537
|
+
});
|
|
2538
|
+
} finally {
|
|
2539
|
+
await session.close();
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
1792
2542
|
function createTileflowCaptureSession(options = {}) {
|
|
1793
2543
|
return new TileflowCaptureSessionImpl(options);
|
|
1794
2544
|
}
|
|
@@ -1918,9 +2668,9 @@ var TileflowCaptureSessionImpl = class {
|
|
|
1918
2668
|
timeoutMs: this.#options.timeoutMs
|
|
1919
2669
|
});
|
|
1920
2670
|
const [sha256, sceneSha256, styleSha256] = await Promise.all([
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
2671
|
+
sha256Hex2(rendered.png),
|
|
2672
|
+
sha256Hex2(serializeCanonicalJson2(scene)),
|
|
2673
|
+
sha256Hex2(serializeCanonicalJson2(style))
|
|
1924
2674
|
]);
|
|
1925
2675
|
const target = scene.target.kind;
|
|
1926
2676
|
const receipt = createTileflowCaptureReceipt({
|
|
@@ -1958,6 +2708,98 @@ var TileflowCaptureSessionImpl = class {
|
|
|
1958
2708
|
}
|
|
1959
2709
|
return { schemaVersion: tileflowCaptureResultSchemaVersion, captures };
|
|
1960
2710
|
}
|
|
2711
|
+
async captureShot(shot, options = {}) {
|
|
2712
|
+
const result = await this.captureShots([shot], options);
|
|
2713
|
+
return result.shots[0];
|
|
2714
|
+
}
|
|
2715
|
+
async captureAllShots(options = {}) {
|
|
2716
|
+
this.#assertOpen();
|
|
2717
|
+
const artifacts = await this.#createArtifacts();
|
|
2718
|
+
try {
|
|
2719
|
+
return await this.#captureShotArtifacts(
|
|
2720
|
+
artifacts,
|
|
2721
|
+
Object.keys(artifacts.project.shots ?? {}),
|
|
2722
|
+
options
|
|
2723
|
+
);
|
|
2724
|
+
} finally {
|
|
2725
|
+
await disposeCaptureArtifacts(artifacts);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
async captureShots(shots, options = {}) {
|
|
2729
|
+
this.#assertOpen();
|
|
2730
|
+
const artifacts = await this.#createArtifacts();
|
|
2731
|
+
try {
|
|
2732
|
+
return await this.#captureShotArtifacts(artifacts, shots, options);
|
|
2733
|
+
} finally {
|
|
2734
|
+
await disposeCaptureArtifacts(artifacts);
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
async #captureShotArtifacts(artifacts, requestedShots, options) {
|
|
2738
|
+
const signal = options.signal ?? this.#options.signal;
|
|
2739
|
+
const format = options.format ?? "webm";
|
|
2740
|
+
if (format !== "webm" && format !== "frames") {
|
|
2741
|
+
throw new TypeError('Expected shot format "webm" or "frames".');
|
|
2742
|
+
}
|
|
2743
|
+
const shotNames = selectTileflowCaptureShotNames(artifacts, requestedShots);
|
|
2744
|
+
const resolved = [];
|
|
2745
|
+
const preparedStyles = /* @__PURE__ */ new Map();
|
|
2746
|
+
for (const name of shotNames) {
|
|
2747
|
+
const shot = normalizeShot(artifacts, name);
|
|
2748
|
+
const themes = [.../* @__PURE__ */ new Set([shot.theme, ...shot.keyframes[0]?.blend?.themes ?? []])];
|
|
2749
|
+
const styles = {};
|
|
2750
|
+
const prepared = [];
|
|
2751
|
+
for (const theme of themes) {
|
|
2752
|
+
const styleKey = `${shot.map}/${theme}`;
|
|
2753
|
+
let preparedStyle = preparedStyles.get(styleKey);
|
|
2754
|
+
if (!preparedStyle) {
|
|
2755
|
+
const style = artifacts.styles[shot.map]?.[theme];
|
|
2756
|
+
if (!style) {
|
|
2757
|
+
throw new TileflowCaptureError(
|
|
2758
|
+
"SHOT_NOT_FOUND",
|
|
2759
|
+
`Shot "${name}" references unavailable theme "${theme}" on map "${shot.map}".`
|
|
2760
|
+
);
|
|
2761
|
+
}
|
|
2762
|
+
preparedStyle = await this.#worldSession.prepare(style, signal);
|
|
2763
|
+
try {
|
|
2764
|
+
assertValidTileflowStyle2(preparedStyle.style, styleKey);
|
|
2765
|
+
} catch (error) {
|
|
2766
|
+
throwCaptureStyleValidationError(error);
|
|
2767
|
+
}
|
|
2768
|
+
preparedStyles.set(styleKey, preparedStyle);
|
|
2769
|
+
}
|
|
2770
|
+
styles[theme] = preparedStyle.style;
|
|
2771
|
+
prepared.push(preparedStyle);
|
|
2772
|
+
}
|
|
2773
|
+
resolved.push({ name, prepared, shot, styles });
|
|
2774
|
+
}
|
|
2775
|
+
throwIfAborted2(signal);
|
|
2776
|
+
const browser = await this.#browserManager.getBrowser(signal);
|
|
2777
|
+
const renderer = createTileflowCaptureRendererIdentity(browser);
|
|
2778
|
+
const shots = [];
|
|
2779
|
+
for (const { name, prepared, shot, styles } of resolved) {
|
|
2780
|
+
throwIfAborted2(signal);
|
|
2781
|
+
const capture = await renderTileflowShot({
|
|
2782
|
+
assets: [...artifacts.assets, ...prepared.flatMap((style) => style.assets ?? [])],
|
|
2783
|
+
browser,
|
|
2784
|
+
data: prepared[0].data,
|
|
2785
|
+
format,
|
|
2786
|
+
...artifacts.localTilesets ? { localTilesets: artifacts.localTilesets } : {},
|
|
2787
|
+
...artifacts.lineBackgroundGlyphs ? {
|
|
2788
|
+
glyphCacheDirectory: resolve(this.#options.cwd ?? process.cwd()),
|
|
2789
|
+
lineBackgroundGlyphs: artifacts.lineBackgroundGlyphs
|
|
2790
|
+
} : {},
|
|
2791
|
+
name,
|
|
2792
|
+
...options.onFrame ? { onFrame: options.onFrame } : {},
|
|
2793
|
+
renderer,
|
|
2794
|
+
shot,
|
|
2795
|
+
...signal ? { signal } : {},
|
|
2796
|
+
styles,
|
|
2797
|
+
...this.#options.timeoutMs === void 0 ? {} : { timeoutMs: this.#options.timeoutMs }
|
|
2798
|
+
});
|
|
2799
|
+
shots.push({ ...capture, warnings: [...capture.warnings].sort(compareCodeUnits3) });
|
|
2800
|
+
}
|
|
2801
|
+
return { schemaVersion: tileflowCaptureResultSchemaVersion, shots };
|
|
2802
|
+
}
|
|
1961
2803
|
async close() {
|
|
1962
2804
|
if (this.#closed) {
|
|
1963
2805
|
return;
|
|
@@ -2046,6 +2888,53 @@ function selectTileflowCaptureSceneNames(artifacts, requestedScenes) {
|
|
|
2046
2888
|
}
|
|
2047
2889
|
return sceneNames;
|
|
2048
2890
|
}
|
|
2891
|
+
function selectTileflowCaptureShotNames(artifacts, requestedShots) {
|
|
2892
|
+
const availableShots = Object.keys(artifacts.project.shots ?? {}).filter((shotName) => tileflowCaptureShotNameSchema.safeParse(shotName).success).sort(compareCodeUnits3);
|
|
2893
|
+
const shotNames = [...new Set(requestedShots)].sort(compareCodeUnits3);
|
|
2894
|
+
if (shotNames.length === 0) {
|
|
2895
|
+
throw new TileflowCaptureError(
|
|
2896
|
+
"SHOT_NOT_FOUND",
|
|
2897
|
+
"Select at least one capture shot or request every configured shot explicitly."
|
|
2898
|
+
);
|
|
2899
|
+
}
|
|
2900
|
+
for (const shotName of shotNames) {
|
|
2901
|
+
if (!tileflowCaptureShotNameSchema.safeParse(shotName).success) {
|
|
2902
|
+
throw new TileflowCaptureError("SHOT_NOT_FOUND", "Expected a portable Tileflow shot name.");
|
|
2903
|
+
}
|
|
2904
|
+
if (!Object.hasOwn(artifacts.project.shots ?? {}, shotName)) {
|
|
2905
|
+
const suffix = availableShots.length > 0 ? ` Available shots: ${availableShots.join(", ")}.` : " The config defines no capture shots.";
|
|
2906
|
+
throw new TileflowCaptureError(
|
|
2907
|
+
"SHOT_NOT_FOUND",
|
|
2908
|
+
`Unknown Tileflow capture shot "${shotName}".${suffix}`
|
|
2909
|
+
);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
return shotNames;
|
|
2913
|
+
}
|
|
2914
|
+
function normalizeShot(artifacts, shotName) {
|
|
2915
|
+
const shot = artifacts.project.shots[shotName];
|
|
2916
|
+
const normalized = normalizeTileflowCaptureShot(shot);
|
|
2917
|
+
const resolvedMap = artifacts.project.maps[normalized.map];
|
|
2918
|
+
if (!resolvedMap) {
|
|
2919
|
+
throw new TileflowCaptureError(
|
|
2920
|
+
"SHOT_NOT_FOUND",
|
|
2921
|
+
`Shot "${shotName}" references unavailable map "${normalized.map}".`
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
const map = parseResolvedTileflowMap(resolvedMap);
|
|
2925
|
+
for (const theme of [normalized.theme, ...normalized.keyframes[0]?.blend?.themes ?? []]) {
|
|
2926
|
+
try {
|
|
2927
|
+
resolveThemeSelection(map, theme);
|
|
2928
|
+
} catch (error) {
|
|
2929
|
+
throw new TileflowCaptureError(
|
|
2930
|
+
"SHOT_NOT_FOUND",
|
|
2931
|
+
`Shot "${shotName}" has an invalid theme: ${error instanceof Error ? error.message : "unknown theme"}`,
|
|
2932
|
+
{ cause: error }
|
|
2933
|
+
);
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
return normalized;
|
|
2937
|
+
}
|
|
2049
2938
|
function normalizeScene(artifacts, sceneName) {
|
|
2050
2939
|
const scene = Object.hasOwn(artifacts.project.scenes ?? {}, sceneName) ? artifacts.project.scenes?.[sceneName] : void 0;
|
|
2051
2940
|
if (!scene) {
|
|
@@ -2096,8 +2985,8 @@ import { types as nodeUtilTypes } from "util";
|
|
|
2096
2985
|
import { PNG } from "pngjs";
|
|
2097
2986
|
import {
|
|
2098
2987
|
compareCodeUnits as compareCodeUnits4,
|
|
2099
|
-
serializeCanonicalJson as
|
|
2100
|
-
sha256Hex as
|
|
2988
|
+
serializeCanonicalJson as serializeCanonicalJson3,
|
|
2989
|
+
sha256Hex as sha256Hex3,
|
|
2101
2990
|
tileflowCaptureSceneLimits as tileflowCaptureSceneLimits2
|
|
2102
2991
|
} from "@tileflow/core";
|
|
2103
2992
|
var tileflowVisualComparisonSchemaVersion = 1;
|
|
@@ -2190,9 +3079,6 @@ function validateTileflowVisualReferencePng(png) {
|
|
|
2190
3079
|
async function analyzeTileflowCaptureReference(actualCapture, referencePng, options = {}) {
|
|
2191
3080
|
const requestedRegion = snapshotTileflowVisualReferenceAnalysisOptions(options);
|
|
2192
3081
|
const receipt = validateTileflowCaptureReceipt(actualCapture.receipt);
|
|
2193
|
-
if (receipt.schemaVersion !== 4) {
|
|
2194
|
-
throw invalidBaseline("Visual reference analysis requires a schema-v4 capture receipt.");
|
|
2195
|
-
}
|
|
2196
3082
|
const actualImage = await validatePngAgainstReceipt(actualCapture.png, receipt, "actual");
|
|
2197
3083
|
const referenceImage = decodePng(referencePng, "reference");
|
|
2198
3084
|
const dimensionsMatch = referenceImage.width === actualImage.width && referenceImage.height === actualImage.height;
|
|
@@ -2208,7 +3094,7 @@ async function analyzeTileflowCaptureReference(actualCapture, referencePng, opti
|
|
|
2208
3094
|
target: receipt.scene.target,
|
|
2209
3095
|
dimensionsMatch,
|
|
2210
3096
|
reference: {
|
|
2211
|
-
sha256: await
|
|
3097
|
+
sha256: await sha256Hex3(referencePng),
|
|
2212
3098
|
physicalWidth: referenceImage.width,
|
|
2213
3099
|
physicalHeight: referenceImage.height,
|
|
2214
3100
|
palette: createQuantizedPalette(referenceImage.data)
|
|
@@ -2421,7 +3307,7 @@ function createTileflowVisualComparisonDocument(comparison) {
|
|
|
2421
3307
|
return document;
|
|
2422
3308
|
}
|
|
2423
3309
|
function serializeTileflowVisualComparison(comparison) {
|
|
2424
|
-
return `${
|
|
3310
|
+
return `${serializeCanonicalJson3(createTileflowVisualComparisonDocument(comparison))}
|
|
2425
3311
|
`;
|
|
2426
3312
|
}
|
|
2427
3313
|
function imageIdentity(receipt) {
|
|
@@ -2443,7 +3329,7 @@ async function validatePngAgainstReceipt(png, receipt, role) {
|
|
|
2443
3329
|
if (image.width !== receipt.image.physicalWidth || image.height !== receipt.image.physicalHeight) {
|
|
2444
3330
|
throw invalidBaseline(`The ${role} PNG dimensions do not match its receipt.`);
|
|
2445
3331
|
}
|
|
2446
|
-
if (await
|
|
3332
|
+
if (await sha256Hex3(png) !== receipt.image.sha256) {
|
|
2447
3333
|
throw invalidBaseline(`The ${role} PNG hash does not match its receipt.`);
|
|
2448
3334
|
}
|
|
2449
3335
|
return image;
|
|
@@ -2765,45 +3651,7 @@ function pixelMetric(changedPixels, totalPixels) {
|
|
|
2765
3651
|
return { changedPixels, totalPixels, ratio: changedPixels / totalPixels };
|
|
2766
3652
|
}
|
|
2767
3653
|
function sameSceneIdentity(baseline, actual) {
|
|
2768
|
-
return baseline.scene.name === actual.scene.name && baseline.scene.map === actual.scene.map && baseline.scene.target === actual.scene.target && baseline.scene.sha256 === actual.scene.sha256 &&
|
|
2769
|
-
}
|
|
2770
|
-
function sameDataIdentity(baseline, actual) {
|
|
2771
|
-
if (baseline.kind !== actual.kind || baseline.schema !== actual.schema || baseline.schemaVersion !== actual.schemaVersion || baseline.sourceId !== actual.sourceId) {
|
|
2772
|
-
return false;
|
|
2773
|
-
}
|
|
2774
|
-
const baselineExactWorld = exactWorldIdentity(baseline);
|
|
2775
|
-
const actualExactWorld = exactWorldIdentity(actual);
|
|
2776
|
-
if (baselineExactWorld || actualExactWorld) {
|
|
2777
|
-
if (!baselineExactWorld || !actualExactWorld) return false;
|
|
2778
|
-
return baselineExactWorld.product === actualExactWorld.product && baselineExactWorld.releaseId === actualExactWorld.releaseId && baselineExactWorld.descriptorSha256 === actualExactWorld.descriptorSha256 && baselineExactWorld.archiveSha256 === actualExactWorld.archiveSha256 && baselineExactWorld.dataContractSha256 === actualExactWorld.dataContractSha256 && baselineExactWorld.contractSha256 === actualExactWorld.contractSha256 && sameOptionalSemantics(baselineExactWorld.semantics, actualExactWorld.semantics);
|
|
2779
|
-
}
|
|
2780
|
-
const baselineLegacy = baseline;
|
|
2781
|
-
const actualLegacy = actual;
|
|
2782
|
-
if (baselineLegacy.generation !== actualLegacy.generation || baselineLegacy.revision !== actualLegacy.revision || !sameOptionalSemantics(baselineLegacy.semantics, actualLegacy.semantics)) {
|
|
2783
|
-
return false;
|
|
2784
|
-
}
|
|
2785
|
-
if (baselineLegacy.source && (!actualLegacy.source || baselineLegacy.source.kind !== actualLegacy.source.kind || baselineLegacy.source.sha256 !== actualLegacy.source.sha256)) {
|
|
2786
|
-
return false;
|
|
2787
|
-
}
|
|
2788
|
-
if (baselineLegacy.capabilities && (!actualLegacy.capabilities || !isRecordSubset(baselineLegacy.capabilities, actualLegacy.capabilities))) {
|
|
2789
|
-
return false;
|
|
2790
|
-
}
|
|
2791
|
-
if (baselineLegacy.bindings) {
|
|
2792
|
-
if (!actualLegacy.bindings) return false;
|
|
2793
|
-
if (!isRecordSubset(baselineLegacy.bindings.fields, actualLegacy.bindings.fields) || !isRecordSubset(baselineLegacy.bindings.layers, actualLegacy.bindings.layers)) {
|
|
2794
|
-
return false;
|
|
2795
|
-
}
|
|
2796
|
-
}
|
|
2797
|
-
return true;
|
|
2798
|
-
}
|
|
2799
|
-
function exactWorldIdentity(value) {
|
|
2800
|
-
return value.kind === "tileflow-world" && "product" in value ? value : void 0;
|
|
2801
|
-
}
|
|
2802
|
-
function sameOptionalSemantics(baseline, actual) {
|
|
2803
|
-
return baseline === void 0 || baseline.parkLayer === actual?.parkLayer;
|
|
2804
|
-
}
|
|
2805
|
-
function isRecordSubset(baseline, actual) {
|
|
2806
|
-
return Object.entries(baseline).every(([key, value]) => actual[key] === value);
|
|
3654
|
+
return baseline.scene.name === actual.scene.name && baseline.scene.map === actual.scene.map && baseline.scene.theme === actual.scene.theme && baseline.scene.target === actual.scene.target && baseline.scene.sha256 === actual.scene.sha256 && serializeCanonicalJson3(baseline.data) === serializeCanonicalJson3(actual.data);
|
|
2807
3655
|
}
|
|
2808
3656
|
function sameRuntimeIdentity(baseline, actual) {
|
|
2809
3657
|
return baseline.renderer.tileflow === actual.renderer.tileflow && baseline.renderer.maplibre === actual.renderer.maplibre && baseline.renderer.playwright === actual.renderer.playwright && baseline.renderer.chromiumRevision === actual.renderer.chromiumRevision && baseline.renderer.chromiumVersion === actual.renderer.chromiumVersion && baseline.platform.os === actual.platform.os && baseline.platform.architecture === actual.platform.architecture;
|
|
@@ -2826,8 +3674,8 @@ function invalidBaseline(message, cause) {
|
|
|
2826
3674
|
import { types as nodeUtilTypes2 } from "util";
|
|
2827
3675
|
import {
|
|
2828
3676
|
normalizeTileflowCaptureScene as normalizeTileflowCaptureScene2,
|
|
2829
|
-
serializeCanonicalJson as
|
|
2830
|
-
sha256Hex as
|
|
3677
|
+
serializeCanonicalJson as serializeCanonicalJson4,
|
|
3678
|
+
sha256Hex as sha256Hex4
|
|
2831
3679
|
} from "@tileflow/core";
|
|
2832
3680
|
var tileflowVisualReviewSchemaVersion = 1;
|
|
2833
3681
|
var TileflowVisualReviewError = class extends Error {
|
|
@@ -3142,9 +3990,6 @@ async function validateReviewCapture(input, role) {
|
|
|
3142
3990
|
throw invalidReview(`The ${role} capture receipt is invalid.`, error);
|
|
3143
3991
|
}
|
|
3144
3992
|
const receipt = parsedReceipt;
|
|
3145
|
-
if (receipt.schemaVersion !== 4) {
|
|
3146
|
-
throw invalidReview(`The ${role} review capture requires a schema-v4 receipt.`);
|
|
3147
|
-
}
|
|
3148
3993
|
if (definition.map !== receipt.scene.map || definition.theme !== receipt.scene.theme || definition.target.kind !== receipt.scene.target) {
|
|
3149
3994
|
throw invalidReview(`The ${role} review identity does not match its capture receipt.`);
|
|
3150
3995
|
}
|
|
@@ -3155,7 +4000,7 @@ async function validateReviewCapture(input, role) {
|
|
|
3155
4000
|
if (input.capture.scene !== receipt.scene.name || input.capture.map !== receipt.scene.map || input.capture.theme !== receipt.scene.theme || input.capture.target !== receipt.scene.target || input.capture.sha256 !== receipt.image.sha256 || input.capture.sceneSha256 !== receipt.scene.sha256 || input.capture.styleSha256 !== receipt.style.sha256 || input.capture.width !== receipt.image.cssWidth || input.capture.height !== receipt.image.cssHeight || input.capture.dpr !== receipt.image.dpr || input.capture.networkDependent !== receipt.networkDependent || !sameCanonicalValue(input.capture.renderer, receipt.renderer)) {
|
|
3156
4001
|
throw invalidReview(`The ${role} capture metadata does not match its receipt.`);
|
|
3157
4002
|
}
|
|
3158
|
-
const definitionSha256 = await
|
|
4003
|
+
const definitionSha256 = await sha256Hex4(serializeCanonicalJson4(definition));
|
|
3159
4004
|
if (definitionSha256 !== receipt.scene.sha256) {
|
|
3160
4005
|
throw invalidReview(`The ${role} review definition does not match its capture receipt.`);
|
|
3161
4006
|
}
|
|
@@ -3241,7 +4086,7 @@ function reviewWarnings(status, left, right) {
|
|
|
3241
4086
|
return [...new Set(warnings)].sort(compareCodeUnits5);
|
|
3242
4087
|
}
|
|
3243
4088
|
function sameCanonicalValue(left, right) {
|
|
3244
|
-
return
|
|
4089
|
+
return serializeCanonicalJson4(left) === serializeCanonicalJson4(right);
|
|
3245
4090
|
}
|
|
3246
4091
|
function compareCodeUnits5(left, right) {
|
|
3247
4092
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
@@ -3257,26 +4102,33 @@ export {
|
|
|
3257
4102
|
analyzeTileflowCaptureReference,
|
|
3258
4103
|
captureErrorMessage,
|
|
3259
4104
|
captureTileflowScenes,
|
|
4105
|
+
captureTileflowShot,
|
|
3260
4106
|
compareTileflowCaptureToBaseline,
|
|
3261
4107
|
compareTileflowCapturesForReview,
|
|
3262
4108
|
createTileflowCaptureBrowserEnvironment,
|
|
3263
4109
|
createTileflowCaptureReceipt,
|
|
3264
4110
|
createTileflowCaptureRendererIdentity,
|
|
3265
4111
|
createTileflowCaptureSession,
|
|
4112
|
+
createTileflowShotReceipt,
|
|
3266
4113
|
createTileflowVisualComparisonDocument,
|
|
3267
4114
|
createTileflowVisualReferenceAnalysisDocument,
|
|
3268
4115
|
createTileflowVisualReviewDocument,
|
|
3269
4116
|
launchTileflowCaptureBrowser,
|
|
3270
4117
|
parseTileflowCaptureReceipt,
|
|
4118
|
+
parseTileflowShotReceipt,
|
|
3271
4119
|
resolveTileflowApplicationUrl,
|
|
3272
4120
|
resolveTileflowCaptureWorldTileJson,
|
|
3273
4121
|
serializeTileflowCaptureReceipt,
|
|
4122
|
+
serializeTileflowShotReceipt,
|
|
3274
4123
|
serializeTileflowVisualComparison,
|
|
3275
4124
|
setupTileflowCaptureBrowser,
|
|
3276
4125
|
tileflowCaptureReceiptLimits,
|
|
3277
4126
|
tileflowCaptureReceiptSchemaVersion,
|
|
3278
4127
|
tileflowCaptureResultSchemaVersion,
|
|
3279
4128
|
tileflowCaptureRuntime,
|
|
4129
|
+
tileflowShotFramesSha256,
|
|
4130
|
+
tileflowShotReceiptLimits,
|
|
4131
|
+
tileflowShotReceiptSchemaVersion,
|
|
3280
4132
|
tileflowSyntheticAssetOrigin,
|
|
3281
4133
|
tileflowVisualArtifactLimits,
|
|
3282
4134
|
tileflowVisualComparisonSchemaVersion,
|
|
@@ -3286,5 +4138,6 @@ export {
|
|
|
3286
4138
|
tileflowVisualReviewSchemaVersion,
|
|
3287
4139
|
tileflowWorldCurrentTileJsonUrl,
|
|
3288
4140
|
validateTileflowCaptureReceipt,
|
|
4141
|
+
validateTileflowShotReceipt,
|
|
3289
4142
|
validateTileflowVisualReferencePng
|
|
3290
4143
|
};
|