@tileflow/capture 0.1.0-alpha.39 → 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 +10 -0
- package/dist/index.d.ts +43 -2
- package/dist/index.js +351 -27
- package/package.json +10 -9
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
|
|
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,
|
|
@@ -379,6 +395,7 @@ globalThis.__tileflowAttachNotices = attachTileflowNotices;
|
|
|
379
395
|
globalThis.__tileflowAttachNauticalLights = attachTileflowNauticalLights;
|
|
380
396
|
globalThis.__tileflowMaxPitch = tileflowMaxPitch;
|
|
381
397
|
globalThis.__tileflowAttachLabelPlacement = attachTileflowLabelPlacement;
|
|
398
|
+
globalThis.__tileflowCreateMap3D = createTileflowMap3D;
|
|
382
399
|
globalThis.__tileflowCreateThemeController = createTileflowThemeController;`;
|
|
383
400
|
async function captureStandaloneTileflowScene(input) {
|
|
384
401
|
if (input.scene.target.kind !== "map") {
|
|
@@ -430,6 +447,8 @@ async function withStandaloneTileflowPage(input, run) {
|
|
|
430
447
|
let context;
|
|
431
448
|
let phase = "browser-start";
|
|
432
449
|
let timeout;
|
|
450
|
+
let budgetStartedAt = Date.now();
|
|
451
|
+
const pageWarnings = /* @__PURE__ */ new Set();
|
|
433
452
|
const onAbort = () => {
|
|
434
453
|
termination = "aborted";
|
|
435
454
|
void context?.close().catch(() => void 0);
|
|
@@ -440,6 +459,7 @@ async function withStandaloneTileflowPage(input, run) {
|
|
|
440
459
|
};
|
|
441
460
|
const restartTimeout = () => {
|
|
442
461
|
stopTimeout();
|
|
462
|
+
budgetStartedAt = Date.now();
|
|
443
463
|
timeout = setTimeout(() => {
|
|
444
464
|
termination = "timeout";
|
|
445
465
|
void context?.close().catch(() => void 0);
|
|
@@ -484,6 +504,11 @@ async function withStandaloneTileflowPage(input, run) {
|
|
|
484
504
|
const resourceFailures = [];
|
|
485
505
|
let pageFailed = false;
|
|
486
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
|
+
}
|
|
487
512
|
const diagnostic = classifyResource(url, status);
|
|
488
513
|
if (!diagnostic || resourceFailures.length >= 16) return;
|
|
489
514
|
const key = JSON.stringify(diagnostic);
|
|
@@ -591,7 +616,10 @@ async function withStandaloneTileflowPage(input, run) {
|
|
|
591
616
|
recordResourceFailure(response.url(), response.status());
|
|
592
617
|
}
|
|
593
618
|
});
|
|
594
|
-
page.on("requestfailed", (request) =>
|
|
619
|
+
page.on("requestfailed", (request) => {
|
|
620
|
+
if (request.failure()?.errorText === "net::ERR_ABORTED") return;
|
|
621
|
+
recordResourceFailure(request.url());
|
|
622
|
+
});
|
|
595
623
|
page.setDefaultTimeout(timeoutMs);
|
|
596
624
|
await page.goto(mapLibreRuntimeUrls.document, { waitUntil: "domcontentloaded" });
|
|
597
625
|
await page.addStyleTag({ url: mapLibreRuntimeUrls.css });
|
|
@@ -606,16 +634,31 @@ globalThis.maplibregl = maplibregl;`,
|
|
|
606
634
|
);
|
|
607
635
|
await page.addScriptTag({ content: tileflowBrowserModule, type: "module" });
|
|
608
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
|
+
}
|
|
609
647
|
const step = async (stepPhase, name, value) => {
|
|
610
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);
|
|
611
651
|
const result = await page.evaluate(
|
|
612
652
|
({ name: functionName, value: stepInput }) => {
|
|
613
653
|
const renderWindow = window;
|
|
614
654
|
return renderWindow[functionName](stepInput);
|
|
615
655
|
},
|
|
616
|
-
{ name, value }
|
|
656
|
+
{ name, value: { ...value, sceneBudgetMs } }
|
|
617
657
|
);
|
|
618
658
|
assertPhaseSucceeded(result, stepPhase, syntheticAssetFailure, resourceFailures, pageFailed);
|
|
659
|
+
if (result.status === "ok") {
|
|
660
|
+
for (const warning of result.warnings ?? []) pageWarnings.add(safeSceneWarning(warning));
|
|
661
|
+
}
|
|
619
662
|
};
|
|
620
663
|
return await run({
|
|
621
664
|
page,
|
|
@@ -663,7 +706,10 @@ globalThis.maplibregl = maplibregl;`,
|
|
|
663
706
|
result() {
|
|
664
707
|
return {
|
|
665
708
|
networkDependent: remoteOrigins.size > 0,
|
|
666
|
-
warnings: [
|
|
709
|
+
warnings: [
|
|
710
|
+
...[...remoteOrigins].sort(compareCodeUnits).map((origin) => `Capture requested remote resources from ${origin}.`),
|
|
711
|
+
...[...pageWarnings].sort(compareCodeUnits)
|
|
712
|
+
]
|
|
667
713
|
};
|
|
668
714
|
}
|
|
669
715
|
});
|
|
@@ -672,10 +718,11 @@ globalThis.maplibregl = maplibregl;`,
|
|
|
672
718
|
throw new TileflowCaptureError("ABORTED", "Tileflow capture was aborted.", { cause: error });
|
|
673
719
|
}
|
|
674
720
|
if (termination === "timeout") {
|
|
675
|
-
throw new TileflowCaptureError(
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
721
|
+
throw new TileflowCaptureError(
|
|
722
|
+
"CAPTURE_TIMEOUT",
|
|
723
|
+
`${timeoutMessage(phase)}${slowSceneHint(input.style, input.graphics, phase)}`,
|
|
724
|
+
{ cause: error, details: { phase } }
|
|
725
|
+
);
|
|
679
726
|
}
|
|
680
727
|
if (error instanceof CallerFailure) {
|
|
681
728
|
throw error.error;
|
|
@@ -926,6 +973,41 @@ function classifyResource(value, status) {
|
|
|
926
973
|
...status === void 0 ? {} : { status }
|
|
927
974
|
};
|
|
928
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
|
+
}
|
|
929
1011
|
function fontStackContext(path) {
|
|
930
1012
|
const segments = path.split("/").filter(Boolean);
|
|
931
1013
|
const index = segments.findIndex((segment) => /^(?:fonts?|glyphs?)$/i.test(segment));
|
|
@@ -952,6 +1034,20 @@ function resourceLabel(kind) {
|
|
|
952
1034
|
"vector-tile": "Vector tile"
|
|
953
1035
|
}[kind];
|
|
954
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
|
+
}
|
|
955
1051
|
function timeoutMessage(phase) {
|
|
956
1052
|
if (phase === "map-load") return "MapLibre did not finish loading before the capture timeout.";
|
|
957
1053
|
if (phase === "theme-blend") return "The theme blend was not ready before the capture timeout.";
|
|
@@ -967,6 +1063,9 @@ function mapFailureMessage(phase) {
|
|
|
967
1063
|
function safeMapError(value) {
|
|
968
1064
|
return value.replace(/[A-Za-z][A-Za-z0-9+.-]*:\/\/\S+/g, "resource").replace(/[^A-Za-z0-9 _.,:'"@+()[\]/=-]/g, "").trim().slice(0, 200);
|
|
969
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
|
+
}
|
|
970
1069
|
function safeAssetName(value) {
|
|
971
1070
|
return value.replace(/[^A-Za-z0-9._/-]/g, "").slice(0, 200) || "unknown";
|
|
972
1071
|
}
|
|
@@ -1049,6 +1148,17 @@ window.__tileflowCaptureLoad = async function(input) {
|
|
|
1049
1148
|
// Capture prefers reduced motion, so Nautical lights stay steady and every image is stable.
|
|
1050
1149
|
window.__tileflowAttachNauticalLights(map);
|
|
1051
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
|
+
});
|
|
1052
1162
|
return await waitForMapEvent(map, "load", input.mapEventTimeoutMs);
|
|
1053
1163
|
} catch (error) {
|
|
1054
1164
|
return {status: "failed", reason: "error", message: String(error?.message || error || "")};
|
|
@@ -1106,14 +1216,11 @@ async function loadTileflowFontFaces(definitions) {
|
|
|
1106
1216
|
window.__tileflowCaptureIdle = async function(input) {
|
|
1107
1217
|
const map = window.__tileflowMap;
|
|
1108
1218
|
if (!map) return {status: "failed", reason: "error"};
|
|
1219
|
+
releaseFrames(map);
|
|
1109
1220
|
|
|
1110
1221
|
try {
|
|
1111
1222
|
applyCamera(map, input.camera);
|
|
1112
|
-
|
|
1113
|
-
if (result.status === "failed") return result;
|
|
1114
|
-
await nextFrame();
|
|
1115
|
-
await nextFrame();
|
|
1116
|
-
return {status: "ok"};
|
|
1223
|
+
return await settleMap(map, input);
|
|
1117
1224
|
} catch {
|
|
1118
1225
|
return {status: "failed", reason: "error"};
|
|
1119
1226
|
}
|
|
@@ -1123,6 +1230,7 @@ window.__tileflowCaptureIdle = async function(input) {
|
|
|
1123
1230
|
window.__tileflowCapturePrepareBlend = async function(input) {
|
|
1124
1231
|
const map = window.__tileflowMap;
|
|
1125
1232
|
if (!map) return {status: "failed", reason: "error"};
|
|
1233
|
+
releaseFrames(map);
|
|
1126
1234
|
|
|
1127
1235
|
try {
|
|
1128
1236
|
const controller = window.__tileflowCreateThemeController({
|
|
@@ -1147,6 +1255,7 @@ window.__tileflowCapturePrepareBlend = async function(input) {
|
|
|
1147
1255
|
window.__tileflowCaptureFrame = async function(input) {
|
|
1148
1256
|
const map = window.__tileflowMap;
|
|
1149
1257
|
if (!map) return {status: "failed", reason: "error"};
|
|
1258
|
+
releaseFrames(map);
|
|
1150
1259
|
|
|
1151
1260
|
try {
|
|
1152
1261
|
const blend = window.__tileflowShotBlend;
|
|
@@ -1161,17 +1270,110 @@ window.__tileflowCaptureFrame = async function(input) {
|
|
|
1161
1270
|
}
|
|
1162
1271
|
applyCamera(map, input.camera);
|
|
1163
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) {
|
|
1164
1285
|
const result = await waitForMapEvent(map, "idle", input.mapEventTimeoutMs);
|
|
1165
1286
|
if (result.status === "failed") return result;
|
|
1166
1287
|
await nextFrame();
|
|
1167
1288
|
await nextFrame();
|
|
1168
|
-
return {status: "ok"};
|
|
1169
|
-
} catch {
|
|
1170
|
-
return {status: "failed", reason: "error"};
|
|
1289
|
+
return {status: "ok", warnings: []};
|
|
1171
1290
|
}
|
|
1172
|
-
|
|
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
|
+
}
|
|
1173
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.
|
|
1174
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) {
|
|
1175
1377
|
if (camera.type === "bounds") {
|
|
1176
1378
|
mapInstance.fitBounds(
|
|
1177
1379
|
[
|
|
@@ -1197,7 +1399,8 @@ function applyCamera(mapInstance, camera) {
|
|
|
1197
1399
|
}
|
|
1198
1400
|
|
|
1199
1401
|
function waitForMapEvent(mapInstance, event, timeoutMs) {
|
|
1200
|
-
|
|
1402
|
+
let cancel;
|
|
1403
|
+
const promise = new Promise((resolve) => {
|
|
1201
1404
|
const timeout = window.setTimeout(() => {
|
|
1202
1405
|
cleanup();
|
|
1203
1406
|
resolve({status: "failed", reason: "timeout"});
|
|
@@ -1207,6 +1410,15 @@ function waitForMapEvent(mapInstance, event, timeoutMs) {
|
|
|
1207
1410
|
resolve({status: "ok"});
|
|
1208
1411
|
};
|
|
1209
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
|
+
}
|
|
1210
1422
|
const message = String(errorEvent?.error?.message || errorEvent?.message || "");
|
|
1211
1423
|
const missingSourceLayer = message.match(
|
|
1212
1424
|
/^Source layer "(.+)" does not exist on source "(.+)" as specified by style layer ".+"\.$/
|
|
@@ -1236,9 +1448,27 @@ function waitForMapEvent(mapInstance, event, timeoutMs) {
|
|
|
1236
1448
|
mapInstance.off("error", onError);
|
|
1237
1449
|
};
|
|
1238
1450
|
|
|
1451
|
+
cancel = () => {
|
|
1452
|
+
cleanup();
|
|
1453
|
+
resolve({status: "cancelled"});
|
|
1454
|
+
};
|
|
1239
1455
|
mapInstance.once(event, onSuccess);
|
|
1240
1456
|
mapInstance.on("error", onError);
|
|
1241
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
|
+
}
|
|
1242
1472
|
}
|
|
1243
1473
|
|
|
1244
1474
|
function isLoopbackSourceUrl(value) {
|
|
@@ -1658,6 +1888,90 @@ function compareCodeUnits2(left, right) {
|
|
|
1658
1888
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
1659
1889
|
}
|
|
1660
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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1661
1975
|
// src/shot.ts
|
|
1662
1976
|
import {
|
|
1663
1977
|
serializeCanonicalJson,
|
|
@@ -2610,6 +2924,7 @@ var TileflowCaptureSessionImpl = class {
|
|
|
2610
2924
|
this.#options = options;
|
|
2611
2925
|
this.#browserManager = new TileflowCaptureBrowserManager({
|
|
2612
2926
|
allowInstall: options.allowBrowserInstall,
|
|
2927
|
+
...options.graphics ? { graphics: options.graphics } : {},
|
|
2613
2928
|
onInstallProgress: options.onBrowserInstallProgress,
|
|
2614
2929
|
signal: options.signal
|
|
2615
2930
|
});
|
|
@@ -2721,6 +3036,7 @@ var TileflowCaptureSessionImpl = class {
|
|
|
2721
3036
|
glyphCacheDirectory: resolve(this.#options.cwd ?? process.cwd()),
|
|
2722
3037
|
lineBackgroundGlyphs: artifacts.lineBackgroundGlyphs
|
|
2723
3038
|
} : {},
|
|
3039
|
+
...this.#options.graphics ? { graphics: this.#options.graphics } : {},
|
|
2724
3040
|
scene,
|
|
2725
3041
|
signal,
|
|
2726
3042
|
style,
|
|
@@ -2860,6 +3176,12 @@ var TileflowCaptureSessionImpl = class {
|
|
|
2860
3176
|
}
|
|
2861
3177
|
return { schemaVersion: tileflowCaptureResultSchemaVersion, shots };
|
|
2862
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
|
+
}
|
|
2863
3185
|
async close() {
|
|
2864
3186
|
if (this.#closed) {
|
|
2865
3187
|
return;
|
|
@@ -3219,8 +3541,8 @@ async function analyzeTileflowCaptureReference(actualCapture, referencePng, opti
|
|
|
3219
3541
|
};
|
|
3220
3542
|
}
|
|
3221
3543
|
function createTileflowVisualReferenceAnalysisDocument(analysis) {
|
|
3222
|
-
const { diffPng: _diffPng, ...
|
|
3223
|
-
return
|
|
3544
|
+
const { diffPng: _diffPng, ...document2 } = analysis;
|
|
3545
|
+
return document2;
|
|
3224
3546
|
}
|
|
3225
3547
|
async function compareTileflowCaptureToBaseline(actualCapture, baseline) {
|
|
3226
3548
|
const actualReceipt = validateTileflowCaptureReceipt(actualCapture.receipt);
|
|
@@ -3363,8 +3685,8 @@ async function compareTileflowCaptureToBaseline(actualCapture, baseline) {
|
|
|
3363
3685
|
};
|
|
3364
3686
|
}
|
|
3365
3687
|
function createTileflowVisualComparisonDocument(comparison) {
|
|
3366
|
-
const { diffPng: _diffPng, ...
|
|
3367
|
-
return
|
|
3688
|
+
const { diffPng: _diffPng, ...document2 } = comparison;
|
|
3689
|
+
return document2;
|
|
3368
3690
|
}
|
|
3369
3691
|
function serializeTileflowVisualComparison(comparison) {
|
|
3370
3692
|
return `${serializeCanonicalJson3(createTileflowVisualComparisonDocument(comparison))}
|
|
@@ -3796,8 +4118,8 @@ async function compareTileflowCapturesForReview(leftInput, rightInput, options =
|
|
|
3796
4118
|
};
|
|
3797
4119
|
}
|
|
3798
4120
|
function createTileflowVisualReviewDocument(comparison) {
|
|
3799
|
-
const { diffPng: _diffPng, ...
|
|
3800
|
-
return
|
|
4121
|
+
const { diffPng: _diffPng, ...document2 } = comparison;
|
|
4122
|
+
return document2;
|
|
3801
4123
|
}
|
|
3802
4124
|
var tileflowVisualReviewLimits = Object.freeze({
|
|
3803
4125
|
maximumAggregatePngBytes: tileflowVisualArtifactLimits.maximumPngBytes
|
|
@@ -4187,6 +4509,7 @@ export {
|
|
|
4187
4509
|
launchTileflowCaptureBrowser,
|
|
4188
4510
|
parseTileflowCaptureReceipt,
|
|
4189
4511
|
parseTileflowShotReceipt,
|
|
4512
|
+
renderTileflowContactSheet,
|
|
4190
4513
|
resolveTileflowApplicationUrl,
|
|
4191
4514
|
resolveTileflowCaptureWorldTileJson,
|
|
4192
4515
|
serializeTileflowCaptureReceipt,
|
|
@@ -4197,6 +4520,7 @@ export {
|
|
|
4197
4520
|
tileflowCaptureReceiptSchemaVersion,
|
|
4198
4521
|
tileflowCaptureResultSchemaVersion,
|
|
4199
4522
|
tileflowCaptureRuntime,
|
|
4523
|
+
tileflowContactSheetLimits,
|
|
4200
4524
|
tileflowShotFramesSha256,
|
|
4201
4525
|
tileflowShotReceiptLimits,
|
|
4202
4526
|
tileflowShotReceiptSchemaVersion,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tileflow/capture",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
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.
|
|
51
|
-
"@tileflow/dev": ">=0.1.0-alpha.
|
|
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",
|
|
@@ -69,12 +69,13 @@
|
|
|
69
69
|
"vite": "^8.3.4",
|
|
70
70
|
"vue": "^3.5.43",
|
|
71
71
|
"webpack": "5.108.3",
|
|
72
|
-
"@tileflow/
|
|
73
|
-
"@tileflow/
|
|
74
|
-
"@tileflow/
|
|
75
|
-
"@tileflow/svelte": "0.1.0-alpha.
|
|
76
|
-
"@tileflow/
|
|
77
|
-
"@tileflow/vite": "0.1.0-alpha.
|
|
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",
|
|
78
79
|
"@tileflow/webpack": "0.1.0-alpha.25"
|
|
79
80
|
},
|
|
80
81
|
"scripts": {
|