@loaders.gl/tile-converter 5.0.0-alpha.11 → 5.0.0-alpha.14

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.
Files changed (152) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +180 -4
  3. package/bin/converter-v5.js +2 -0
  4. package/dist/converter.min.cjs +172 -182
  5. package/dist/i3s-server/bin/i3s-server.min.cjs +113 -158
  6. package/dist/index.cjs +18832 -19228
  7. package/dist/index.js +19566 -19968
  8. package/dist/v5/adapters.cjs +2002 -230
  9. package/dist/v5/adapters.d.ts +20 -0
  10. package/dist/v5/adapters.js +2005 -214
  11. package/dist/v5/archive-source.d.ts +36 -0
  12. package/dist/v5/browser-tileset-source.d.ts +55 -11
  13. package/dist/v5/browser.cjs +18 -0
  14. package/dist/v5/browser.d.ts +15 -1
  15. package/dist/v5/browser.js +26 -0
  16. package/dist/v5/conversion-api.d.ts +34 -0
  17. package/dist/v5/converter-cli.d.ts +1 -0
  18. package/dist/v5/converter-cli.js +1230 -0
  19. package/dist/v5/core.cjs +547 -77
  20. package/dist/v5/core.d.ts +3 -1
  21. package/dist/v5/core.js +540 -70
  22. package/dist/v5/feature-arrow.d.ts +1 -1
  23. package/dist/v5/i3s-mesh-conversion.d.ts +5 -1
  24. package/dist/v5/i3s-mesh-source-features.d.ts +2 -2
  25. package/dist/v5/i3s-mesh-source-material.d.ts +1 -1
  26. package/dist/v5/i3s-mesh-source.d.ts +2 -2
  27. package/dist/v5/i3s-selection-source.d.ts +70 -0
  28. package/dist/v5/index.cjs +15 -0
  29. package/dist/v5/index.d.ts +13 -1
  30. package/dist/v5/index.js +22 -0
  31. package/dist/v5/mesh-conversion.d.ts +21 -3
  32. package/dist/v5/mesh-draco.d.ts +2 -2
  33. package/dist/v5/mesh-features.d.ts +2 -2
  34. package/dist/v5/mesh-hierarchy.d.ts +47 -0
  35. package/dist/v5/mesh-source-features.d.ts +6 -5
  36. package/dist/v5/mesh-source-schema.d.ts +12 -0
  37. package/dist/v5/mesh-source-texture.d.ts +3 -3
  38. package/dist/v5/mesh-source.d.ts +10 -7
  39. package/dist/v5/mesh-tileset-sink.d.ts +5 -0
  40. package/dist/v5/mesh.d.ts +1 -1
  41. package/dist/v5/node-cli.d.ts +9 -0
  42. package/dist/v5/node-conversion.d.ts +34 -0
  43. package/dist/v5/node-feature-schema.d.ts +3 -0
  44. package/dist/v5/node-i3s-source.d.ts +41 -0
  45. package/dist/v5/node-source.d.ts +47 -0
  46. package/dist/v5/node.cjs +913 -0
  47. package/dist/v5/node.d.ts +6 -0
  48. package/dist/v5/node.js +890 -0
  49. package/dist/v5/point-cloud-attributes.d.ts +19 -0
  50. package/dist/v5/point-cloud-conversion.d.ts +1 -1
  51. package/dist/v5/point-cloud-copc.d.ts +44 -0
  52. package/dist/v5/point-cloud-file.d.ts +83 -0
  53. package/dist/v5/point-cloud-potree.d.ts +47 -0
  54. package/dist/v5/point-cloud-source-encoder.d.ts +1 -1
  55. package/dist/v5/point-cloud-source.d.ts +1 -1
  56. package/dist/v5/point-cloud-tileset.d.ts +44 -0
  57. package/dist/v5/point-cloud.d.ts +1 -1
  58. package/dist/v5/resource-manifest.d.ts +10 -1
  59. package/dist/v5/selected-mesh-source.d.ts +53 -0
  60. package/dist/v5/spatial-conversion.d.ts +1 -1
  61. package/dist/v5/tileset-source.d.ts +1 -1
  62. package/package.json +25 -32
  63. package/src/index.ts +4 -1
  64. package/src/v4/3d-tiles-converter/3d-tiles-converter.ts +11 -12
  65. package/src/v4/3d-tiles-converter/helpers/3d-tiles-content-converter.ts +7 -3
  66. package/src/v4/3d-tiles-converter/helpers/i3s-obb-to-3d-tiles-obb.ts +2 -2
  67. package/src/v4/3d-tiles-converter/helpers/load-i3s.ts +4 -6
  68. package/src/v4/3d-tiles-converter/helpers/texture-atlas.ts +1 -1
  69. package/src/v4/3d-tiles-converter/helpers/validate-i3s-spatial-reference.ts +2 -2
  70. package/src/v4/3d-tiles-converter/json-templates/tileset.ts +58 -39
  71. package/src/v4/converter-cli.ts +10 -4
  72. package/src/v4/deps-installer/deps-installer.ts +3 -3
  73. package/src/v4/i3s-converter/helpers/attribute-metadata-info.ts +1 -1
  74. package/src/v4/i3s-converter/helpers/batch-ids-extensions.ts +9 -10
  75. package/src/v4/i3s-converter/helpers/conversion-gltf.ts +108 -0
  76. package/src/v4/i3s-converter/helpers/coordinate-converter.ts +6 -5
  77. package/src/v4/i3s-converter/helpers/create-scene-server-path.ts +5 -6
  78. package/src/v4/i3s-converter/helpers/feature-attributes.ts +4 -4
  79. package/src/v4/i3s-converter/helpers/geometry-attributes.ts +1 -1
  80. package/src/v4/i3s-converter/helpers/geometry-converter.ts +69 -77
  81. package/src/v4/i3s-converter/helpers/gltf-attributes.ts +58 -74
  82. package/src/v4/i3s-converter/helpers/load-3d-tiles.ts +6 -54
  83. package/src/v4/i3s-converter/helpers/node-debug.ts +7 -3
  84. package/src/v4/i3s-converter/helpers/node-index-document.ts +11 -9
  85. package/src/v4/i3s-converter/helpers/node-pages.ts +3 -4
  86. package/src/v4/i3s-converter/helpers/preprocess-3d-tiles.ts +2 -2
  87. package/src/v4/i3s-converter/helpers/tileset-traversal.ts +1 -1
  88. package/src/v4/i3s-converter/i3s-converter.ts +29 -31
  89. package/src/v4/i3s-converter/json-templates/geometry-definitions.ts +71 -66
  90. package/src/v4/i3s-converter/json-templates/layers.ts +97 -132
  91. package/src/v4/i3s-converter/json-templates/metadata.ts +31 -21
  92. package/src/v4/i3s-converter/json-templates/node.ts +60 -74
  93. package/src/v4/i3s-converter/json-templates/scene-server.ts +36 -27
  94. package/src/v4/i3s-converter/json-templates/shared-resources.ts +62 -123
  95. package/src/v4/i3s-converter/json-templates/store.ts +39 -57
  96. package/src/v4/i3s-converter/types.ts +4 -4
  97. package/src/v4/i3s-server/controllers/slpk-controller.ts +3 -3
  98. package/src/v4/i3s-server/utils/create-scene-server.ts +3 -3
  99. package/src/v4/lib/utils/builder-input.ts +8 -0
  100. package/src/v4/lib/utils/conversion-dump.ts +5 -1
  101. package/src/v4/lib/utils/file-utils.ts +1 -1
  102. package/src/v4/lib/utils/hash-string.ts +21 -0
  103. package/src/v4/lib/utils/lod-conversion-utils.ts +3 -3
  104. package/src/v4/pgm-loader.ts +2 -2
  105. package/src/v5/adapters.ts +53 -0
  106. package/src/v5/archive-source.ts +235 -0
  107. package/src/v5/browser-tileset-source.ts +418 -48
  108. package/src/v5/browser.ts +46 -0
  109. package/src/v5/conversion-api.ts +184 -17
  110. package/src/v5/conversion-archive.ts +2 -2
  111. package/src/v5/converter-cli.ts +28 -0
  112. package/src/v5/core.ts +7 -0
  113. package/src/v5/feature-arrow.ts +2 -2
  114. package/src/v5/i3s-mesh-conversion.ts +41 -3
  115. package/src/v5/i3s-mesh-sink.ts +1 -1
  116. package/src/v5/i3s-mesh-source-features.ts +3 -3
  117. package/src/v5/i3s-mesh-source-material.ts +18 -11
  118. package/src/v5/i3s-mesh-source.ts +3 -2
  119. package/src/v5/i3s-selection-source.ts +298 -0
  120. package/src/v5/index.ts +41 -0
  121. package/src/v5/mesh-conversion.ts +24 -4
  122. package/src/v5/mesh-draco.ts +2 -2
  123. package/src/v5/mesh-features.ts +3 -3
  124. package/src/v5/mesh-hierarchy.ts +648 -0
  125. package/src/v5/mesh-source-features.ts +165 -51
  126. package/src/v5/mesh-source-schema.ts +140 -0
  127. package/src/v5/mesh-source-texture.ts +49 -19
  128. package/src/v5/mesh-source.ts +80 -49
  129. package/src/v5/mesh-tileset-archive.ts +1 -1
  130. package/src/v5/mesh-tileset-sink.ts +2 -2
  131. package/src/v5/mesh.ts +1 -1
  132. package/src/v5/node-cli.ts +201 -0
  133. package/src/v5/node-conversion.ts +362 -0
  134. package/src/v5/node-feature-schema.ts +155 -0
  135. package/src/v5/node-i3s-source.ts +181 -0
  136. package/src/v5/node-source.ts +328 -0
  137. package/src/v5/node.ts +16 -0
  138. package/src/v5/point-cloud-attributes.ts +113 -0
  139. package/src/v5/point-cloud-conversion.ts +1 -1
  140. package/src/v5/point-cloud-copc.ts +289 -0
  141. package/src/v5/point-cloud-file.ts +306 -0
  142. package/src/v5/point-cloud-potree.ts +134 -0
  143. package/src/v5/point-cloud-source-encoder.ts +1 -1
  144. package/src/v5/point-cloud-source.ts +1 -1
  145. package/src/v5/point-cloud-spatial.ts +2 -2
  146. package/src/v5/point-cloud-tileset.ts +137 -0
  147. package/src/v5/point-cloud.ts +3 -3
  148. package/src/v5/resource-fetcher.ts +4 -1
  149. package/src/v5/resource-manifest.ts +40 -5
  150. package/src/v5/selected-mesh-source.ts +384 -0
  151. package/src/v5/spatial-conversion.ts +90 -19
  152. package/src/v5/tileset-source.ts +2 -2
@@ -0,0 +1,134 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {Mesh, MeshArrowTable} from '@loaders.gl/core/schema';
6
+ import {PointCloudTiler, type PointCloudTilerOptions} from '@loaders.gl/core/schema-utils';
7
+ import {encodePotreeDataset} from '@loaders.gl/tiles-3d/potree';
8
+ import type {
9
+ PotreeDataset,
10
+ PotreeWriterNode,
11
+ PotreeWriterOptions
12
+ } from '@loaders.gl/tiles-3d/potree';
13
+ import {encodePotreeDatasetArchive} from './point-cloud-file.js';
14
+
15
+ /** Bounded authoring of one decoded dataset in its already placed native coordinates. */
16
+ export interface ConvertPointCloudToPotreeOptions {
17
+ /** Explicit CRS declaration written verbatim; coordinates are not reprojected. */
18
+ readonly projection: string;
19
+ /** Maximum bytes retained across all gathered node attribute buffers. */
20
+ readonly maxGatheredBytes: number;
21
+ /** Maximum complete encoded dataset bytes, including metadata and hierarchy. */
22
+ readonly maxOutputBytes: number;
23
+ /** Input, index, per-node and node-count limits of the shared dynamic tiler. */
24
+ readonly tiling?: Omit<PointCloudTilerOptions, 'signal'>;
25
+ /** Existing version, precision, spacing and hierarchy controls. */
26
+ readonly potree?: Omit<
27
+ NonNullable<PotreeWriterOptions['potree']>,
28
+ 'projection' | 'signal' | 'maxOutputBytes'
29
+ >;
30
+ /** Also package the completed files as a loaders.gl STORE ZIP32 bundle. */
31
+ readonly archive?: {readonly maxArchiveBytes: number};
32
+ /** Cooperative cancellation during indexing, gathering, writing and packaging. */
33
+ readonly signal?: AbortSignal;
34
+ }
35
+
36
+ /** Complete Potree files; no partial dataset is returned after a failure. */
37
+ export interface ConvertedPointCloudPotree {
38
+ /** Exact input row count, emitted once across the additive octree. */
39
+ readonly pointCount: number;
40
+ /** Number of connected octree nodes, including an empty root for empty input. */
41
+ readonly nodeCount: number;
42
+ /** Metadata and encoded resources; applications own storage. */
43
+ readonly dataset: PotreeDataset;
44
+ /** Optional downloadable bundle; applications own object URL and saving lifetimes. */
45
+ readonly archive?: Blob;
46
+ }
47
+
48
+ /**
49
+ * Connects the portable point tiler to the existing Potree 1.x/2.0 dataset writer.
50
+ * Retains additive octree addresses and every source row exactly once, including terminal
51
+ * overflow. Input must be immutable, unindexed decoded points already placed in the declared
52
+ * CRS. No origins, transforms, reprojection, attribute mappings or surface sampling are inferred.
53
+ * The writer rejects attributes or precision that the selected version cannot represent.
54
+ * Gathered nodes are bounded separately from source buffers, indexes, encoded output and ZIP
55
+ * temporaries. This is in-memory authoring, not streaming ingestion or a peak-heap guarantee.
56
+ * The shared tiler's sample coverage bound is not a qualified viewer LOD/error contract.
57
+ * @param input - One decoded point Mesh or MeshArrowTable, including loader output.
58
+ * @param options - Explicit native CRS, resource budgets and existing writer controls.
59
+ * @returns A complete dataset and optional bundle after releasing the owned tiler.
60
+ */
61
+ export async function convertPointCloudToPotree(
62
+ input: Mesh | MeshArrowTable,
63
+ options: ConvertPointCloudToPotreeOptions
64
+ ): Promise<ConvertedPointCloudPotree> {
65
+ if (typeof options.projection !== 'string' || !options.projection.trim()) {
66
+ throw new Error('Potree authoring requires an explicit native CRS declaration');
67
+ }
68
+ for (const limit of [
69
+ options.maxGatheredBytes,
70
+ options.maxOutputBytes,
71
+ options.archive?.maxArchiveBytes
72
+ ]) {
73
+ if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1)) {
74
+ throw new RangeError('Potree authoring byte limits must be positive safe integers');
75
+ }
76
+ }
77
+ // Required budgets must remain required for JavaScript callers too.
78
+ if (options.maxGatheredBytes === undefined || options.maxOutputBytes === undefined) {
79
+ throw new RangeError('Potree authoring requires gathered and encoded byte limits');
80
+ }
81
+ options.signal?.throwIfAborted();
82
+ const tiler = new PointCloudTiler(input, {
83
+ ...options.tiling,
84
+ maxNodes: Math.min(options.tiling?.maxNodes ?? 100000, options.potree?.maxNodes ?? 100000),
85
+ maxTileBytes: Math.min(
86
+ options.tiling?.maxTileBytes ?? 64 * 1024 * 1024,
87
+ options.maxGatheredBytes
88
+ ),
89
+ signal: options.signal
90
+ });
91
+ const nodes: PotreeWriterNode[] = [];
92
+ let gatheredBytes = 0;
93
+ let pointByteLength = 0;
94
+ let pointCount: number;
95
+ try {
96
+ const pending = [await tiler.getRootNode()];
97
+ pointCount = tiler.pointCount;
98
+ while (pending.length) {
99
+ options.signal?.throwIfAborted();
100
+ const node = pending.pop()!;
101
+ // Root gathering is bounded by maxTileBytes; its packed layout sizes later allocations.
102
+ if (node.pointCount * pointByteLength > options.maxGatheredBytes - gatheredBytes) {
103
+ throw new RangeError('Potree gathered node byte budget exceeded');
104
+ }
105
+ const mesh = await tiler.getTileMesh(node.id);
106
+ const nodeBytes = Array.from(
107
+ new Set(Object.values(mesh.attributes).map(attribute => attribute.value.buffer))
108
+ ).reduce((bytes, buffer) => bytes + buffer.byteLength, 0);
109
+ if (nodeBytes > options.maxGatheredBytes - gatheredBytes) {
110
+ throw new RangeError('Potree gathered node byte budget exceeded');
111
+ }
112
+ if (node.pointCount) pointByteLength = nodeBytes / node.pointCount;
113
+ gatheredBytes += nodeBytes;
114
+ nodes.push({id: node.id, mesh});
115
+ const children = await tiler.getChildNodes(node.id);
116
+ for (let index = children.length - 1; index >= 0; index--) pending.push(children[index]);
117
+ }
118
+ } finally {
119
+ tiler.close();
120
+ }
121
+ const dataset = await encodePotreeDataset(nodes, {
122
+ potree: {
123
+ ...options.potree,
124
+ projection: options.projection,
125
+ maxOutputBytes: options.maxOutputBytes,
126
+ signal: options.signal
127
+ }
128
+ });
129
+ const archive = options.archive
130
+ ? await encodePotreeDatasetArchive(dataset, options.archive.maxArchiveBytes, options.signal)
131
+ : undefined;
132
+ options.signal?.throwIfAborted();
133
+ return {pointCount, nodeCount: nodes.length, dataset, archive};
134
+ }
@@ -7,7 +7,7 @@ import type {
7
7
  PointCloudCoordinateSystem,
8
8
  PointCloudTilesetSource,
9
9
  TilesetSpatialReference
10
- } from '@loaders.gl/tiles';
10
+ } from '@loaders.gl/tiles-3d';
11
11
  import {transformPointCloudSourceTile} from './point-cloud-spatial.js';
12
12
  import type {PointCloudSpatialOptions} from './point-cloud-spatial.js';
13
13
  import {encodePointCloudTileWithMetadata} from './point-cloud.js';
@@ -6,7 +6,7 @@ import type {
6
6
  PointCloudTileContent,
7
7
  PointCloudTileHeader,
8
8
  PointCloudTilesetSource
9
- } from '@loaders.gl/tiles';
9
+ } from '@loaders.gl/tiles-3d';
10
10
 
11
11
  /** A source tile and its decoded point content, including null content when no payload exists. */
12
12
  export type PointCloudSourceTile = {
@@ -3,8 +3,8 @@
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
5
  import {Matrix4} from '@math.gl/core';
6
- import {getSpatialCoordinateFrame, createTilesetSpatialReference} from '@loaders.gl/tiles';
7
- import {convertTableToMesh, makeMeshArrowTable} from '@loaders.gl/schema-utils';
6
+ import {getSpatialCoordinateFrame, createTilesetSpatialReference} from '@loaders.gl/tiles-3d';
7
+ import {convertTableToMesh, makeMeshArrowTable} from '@loaders.gl/core/schema-utils';
8
8
  import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
9
9
  import type {
10
10
  I3SConversionSpatialContext,
@@ -0,0 +1,137 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {Mesh, MeshArrowTable} from '@loaders.gl/core/schema';
6
+ import type {PointCloudTilerOptions} from '@loaders.gl/core/schema-utils';
7
+ import type {TilesetSpatialReference} from '@loaders.gl/tiles-3d';
8
+ import {PointCloudTileSource} from '@loaders.gl/tiles-3d/point-cloud-tile-source';
9
+ import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
10
+ import type {
11
+ BrowserTileConversionFile,
12
+ TileConversionProgress,
13
+ TileConversionReport
14
+ } from '@loaders.gl/tile-converter/v5/core';
15
+ import {convertPointCloudSource} from './point-cloud-conversion.js';
16
+ import {createPointCloudTilesetSink} from './point-cloud-tileset-sink.js';
17
+ import type {PointCloudTilesetSinkOptions} from './point-cloud-tileset-sink.js';
18
+ import {createTileConversionArchive} from './conversion-archive.js';
19
+ import type {TileConversionArchiveOptions} from './conversion-archive.js';
20
+
21
+ /** Bounded authoring of a complete decoded ECEF point dataset. */
22
+ export interface ConvertPointCloudToTilesetOptions extends PointCloudTilesetSinkOptions {
23
+ /** Explicit native EPSG:4978 XYZ meters with ellipsoidal heights. No CRS is inferred. */
24
+ readonly spatialReference: TilesetSpatialReference;
25
+ /** In-memory input, index, gathered-tile and node limits; defaults belong to PointCloudTiler. */
26
+ readonly tiling?: Omit<PointCloudTilerOptions, 'signal'>;
27
+ /** Also create an indexed 3TZ Blob within this final archive-size budget. */
28
+ readonly archive?: Pick<TileConversionArchiveOptions, 'maxArchiveBytes'>;
29
+ /** Cooperative cancellation during indexing, traversal, encoding and packaging. */
30
+ readonly signal?: AbortSignal;
31
+ /** PNTS conversion progress; excludes tileset JSON and optional archive packaging. */
32
+ readonly onProgress?: (progress: TileConversionProgress) => void;
33
+ }
34
+
35
+ /** Completed point authoring result; no partial files are returned on failure. */
36
+ export interface ConvertedPointCloudTileset {
37
+ /** Exact number of input points, each emitted once across the complete collection. */
38
+ readonly pointCount: number;
39
+ /** PNTS resource counts and bytes; input bytes count gathered Arrow column buffers. */
40
+ readonly report: TileConversionReport;
41
+ /** PNTS resources and tileset.json, including JSON in the retained output byte budget. */
42
+ readonly files: readonly BrowserTileConversionFile[];
43
+ /** Optional indexed 3TZ Blob. Applications own saving and object URL lifetimes. */
44
+ readonly archive?: Blob;
45
+ }
46
+
47
+ /**
48
+ * Tiles decoded point Mesh/Arrow data and authors a flat additive 3D Tiles 1.0 collection.
49
+ * All input rows are emitted once, including remaining rows at the tiler's maximum depth.
50
+ * Per-tile RTC centers reduce float32 rounding; geometricError gates measured reconstruction error.
51
+ * The flat collection does not preserve octree LOD or provide a sampling-error guarantee.
52
+ * Input must remain immutable until completion. Arrow normalization can copy buffers; retained
53
+ * input, row indexes, gathered tiles, output Blobs and optional ZIP temporaries are separate memory.
54
+ * This is bounded in-memory authoring, not streaming ingestion or an out-of-core converter.
55
+ * Unsupported PNTS attributes fail; callers must explicitly map or remove them beforehand.
56
+ * @param input - One decoded unindexed point-list with finite floating-point XYZ positions.
57
+ * @param options - Explicit ECEF frame, precision and resource limits, and optional 3TZ packaging.
58
+ * @returns Completed files and report after releasing the privately owned dynamic source.
59
+ */
60
+ export async function convertPointCloudToTileset(
61
+ input: Mesh | MeshArrowTable,
62
+ options: ConvertPointCloudToTilesetOptions
63
+ ): Promise<ConvertedPointCloudTileset> {
64
+ const reference = options.spatialReference;
65
+ if (
66
+ !reference ||
67
+ reference.status !== 'native' ||
68
+ reference.sourceCrs !== 'EPSG:4978' ||
69
+ (reference.targetCrs && reference.targetCrs !== 'EPSG:4978') ||
70
+ reference.coordinateFrame !== 'geocentric' ||
71
+ reference.axisOrder !== 'xyz' ||
72
+ reference.heightReference !== 'ellipsoidal' ||
73
+ reference.verticalUnitScale !== 1 ||
74
+ reference.units?.some(unit => unit !== 'meter')
75
+ ) {
76
+ throw new TileConversionError(
77
+ 'POINT_CLOUD_TILESET_FRAME_UNSUPPORTED',
78
+ 'Point authoring requires explicit native EPSG:4978 xyz meters and ellipsoidal heights'
79
+ );
80
+ }
81
+ if (
82
+ options.archive &&
83
+ (!Number.isSafeInteger(options.archive.maxArchiveBytes) || options.archive.maxArchiveBytes < 0)
84
+ ) {
85
+ throw new TileConversionError(
86
+ 'INVALID_ARCHIVE_BYTE_LIMIT',
87
+ 'Archive byte limit must be a non-negative safe integer'
88
+ );
89
+ }
90
+ options.signal?.throwIfAborted();
91
+ const sink = createPointCloudTilesetSink(options);
92
+ let source: PointCloudTileSource | undefined;
93
+ try {
94
+ source = new PointCloudTileSource(input, {
95
+ pointCloudTiler: {...options.tiling, signal: options.signal, spatialReference: reference}
96
+ });
97
+ const {pointCount} = await source.getMetadata();
98
+ const report = await convertPointCloudSource(source, {
99
+ sink,
100
+ signal: options.signal,
101
+ onProgress: options.onProgress,
102
+ measureInputBytes: tile => {
103
+ const table = tile.content?.data.data;
104
+ return table
105
+ ? table.schema.fields.reduce(
106
+ (bytes, _field, index) => bytes + (table.getChildAt(index)?.byteLength ?? 0),
107
+ 0
108
+ )
109
+ : 0;
110
+ },
111
+ getTileEncodingOptions: tile => ({
112
+ rtcCenter: [
113
+ tile.header.boundingVolume.center[0],
114
+ tile.header.boundingVolume.center[1],
115
+ tile.header.boundingVolume.center[2]
116
+ ],
117
+ maxPositionError: options.geometricError
118
+ })
119
+ });
120
+ options.signal?.throwIfAborted();
121
+ const files = sink.getFiles();
122
+ const archive = options.archive
123
+ ? await createTileConversionArchive(files, {
124
+ format: '3tz',
125
+ maxArchiveBytes: options.archive.maxArchiveBytes,
126
+ signal: options.signal
127
+ })
128
+ : undefined;
129
+ options.signal?.throwIfAborted();
130
+ return {pointCount, report, files, archive};
131
+ } catch (error) {
132
+ await sink.abort(error);
133
+ throw error;
134
+ } finally {
135
+ source?.close();
136
+ }
137
+ }
@@ -2,14 +2,14 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import {Tile3DWriter, TILE3D_TYPE} from '@loaders.gl/3d-tiles';
5
+ import {Tile3DWriter, TILE3D_TYPE} from '@loaders.gl/tiles-3d/3d-tiles';
6
6
  import type {
7
7
  Mesh,
8
8
  MeshArrowTable,
9
9
  MeshAttribute,
10
10
  MeshAttributeQuantizationTransform
11
- } from '@loaders.gl/schema';
12
- import {convertTableToMesh} from '@loaders.gl/schema-utils';
11
+ } from '@loaders.gl/core/schema';
12
+ import {convertTableToMesh} from '@loaders.gl/core/schema-utils';
13
13
  import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
14
14
 
15
15
  /** Options for encoding one Arrow or mesh point batch as a 3D Tiles point tile. */
@@ -101,10 +101,13 @@ export function createTileConversionResourceFetcher(
101
101
  bytes.set(part, offset);
102
102
  offset += part.byteLength;
103
103
  }
104
- return new Response(bytes, {
104
+ const boundedResponse = new Response(bytes, {
105
105
  status: response.status,
106
106
  statusText: response.statusText,
107
107
  headers: response.headers
108
108
  });
109
+ // Relative dependencies must resolve against the final URL after redirects.
110
+ Object.defineProperty(boundedResponse, 'url', {value: response.url});
111
+ return boundedResponse;
109
112
  };
110
113
  }
@@ -43,6 +43,17 @@ export interface ManifestBackedTileConversionSinkOptions<TResource> {
43
43
  readonly sink: TileConversionSink<TResource>;
44
44
  /** Persistence adapter for snapshots written after each resource. */
45
45
  readonly manifestStore: TileResourceManifestStore;
46
+ /** Optional cap on unique checkpoint entries loaded or written; defaults to no cap. */
47
+ readonly maxResources?: number;
48
+ /**
49
+ * Checks destination bytes before skipping a matching checkpoint. Return true only when
50
+ * stored bytes match the entry's size and fingerprint; false rewrites the resource.
51
+ * Without this callback the caller guarantees checkpointed destination bytes remain intact.
52
+ */
53
+ readonly verifyResource?: (
54
+ entry: TileResourceManifestEntry,
55
+ signal?: AbortSignal
56
+ ) => Promise<boolean>;
46
57
  /** Stable profile-specific path or resource key. */
47
58
  readonly getResourceId: (resource: TResource) => string;
48
59
  /** Byte length used to detect changed content under an existing resource ID. */
@@ -62,7 +73,8 @@ export interface ManifestBackedTileConversionSinkOptions<TResource> {
62
73
  * including writes skipped because the matching resource was already present. A graceful abort
63
74
  * clears the manifest checkpoint before asking the destination to clean partial output, so the
64
75
  * next run starts cleanly. A process interruption that does not call `abort` can resume from its
65
- * last saved checkpoint.
76
+ * last saved checkpoint. Optional destination verification repairs missing or changed stored
77
+ * resources before treating them as complete. maxResources caps unique checkpoint entries.
66
78
  *
67
79
  * @param options - Destination, manifest store, stable IDs, byte measurement, and fingerprinting.
68
80
  * @returns A `TileConversionSink` with resumable per-resource progress.
@@ -70,10 +82,17 @@ export interface ManifestBackedTileConversionSinkOptions<TResource> {
70
82
  export async function createManifestBackedTileConversionSink<TResource>(
71
83
  options: ManifestBackedTileConversionSinkOptions<TResource>
72
84
  ): Promise<TileConversionSink<TResource>> {
85
+ const maxResources = options.maxResources ?? Infinity;
86
+ if (maxResources !== Infinity && (!Number.isSafeInteger(maxResources) || maxResources < 0)) {
87
+ throw new TileConversionError(
88
+ 'INVALID_MANIFEST_RESOURCE_LIMIT',
89
+ 'maxResources must be a nonnegative safe integer or Infinity'
90
+ );
91
+ }
73
92
  const priorManifest = await options.manifestStore.load();
74
93
  const resources = new Map<string, TileResourceManifestEntry>();
75
94
  if (priorManifest) {
76
- validateManifest(priorManifest);
95
+ validateManifest(priorManifest, maxResources);
77
96
  for (const entry of priorManifest.resources) {
78
97
  resources.set(entry.resourceId, entry);
79
98
  }
@@ -102,6 +121,7 @@ export async function createManifestBackedTileConversionSink<TResource>(
102
121
  if (writeFailed) {
103
122
  throw writeFailure;
104
123
  }
124
+ signal?.throwIfAborted();
105
125
  const entry = createManifestEntry(options, resource);
106
126
  const priorEntry = resources.get(entry.resourceId);
107
127
  if (priorEntry) {
@@ -114,7 +134,16 @@ export async function createManifestBackedTileConversionSink<TResource>(
114
134
  `Resource "${entry.resourceId}" changed since the saved manifest`
115
135
  );
116
136
  }
117
- return;
137
+ const verified = options.verifyResource
138
+ ? await options.verifyResource({...priorEntry}, signal)
139
+ : true;
140
+ signal?.throwIfAborted();
141
+ if (verified === true) return;
142
+ } else if (resources.size >= maxResources) {
143
+ throw new TileConversionError(
144
+ 'MANIFEST_RESOURCE_LIMIT_EXCEEDED',
145
+ 'Resource checkpoint exceeds maxResources'
146
+ );
118
147
  }
119
148
 
120
149
  await options.sink.write(resource, signal);
@@ -179,7 +208,7 @@ function createManifestEntry<TResource>(
179
208
  }
180
209
 
181
210
  /** Checks a loaded manifest before trusting it for restart decisions. */
182
- function validateManifest(manifest: TileResourceManifest): void {
211
+ function validateManifest(manifest: TileResourceManifest, maxResources: number): void {
183
212
  if (
184
213
  typeof manifest !== 'object' ||
185
214
  manifest === null ||
@@ -189,7 +218,13 @@ function validateManifest(manifest: TileResourceManifest): void {
189
218
  ) {
190
219
  throw new TileConversionError(
191
220
  'UNSUPPORTED_RESOURCE_MANIFEST',
192
- `Unsupported or malformed resource manifest version: ${String(manifest.version)}`
221
+ `Unsupported or malformed resource manifest version: ${String(manifest?.version)}`
222
+ );
223
+ }
224
+ if (manifest.resources.length > maxResources) {
225
+ throw new TileConversionError(
226
+ 'MANIFEST_RESOURCE_LIMIT_EXCEEDED',
227
+ 'Loaded manifest exceeds maxResources'
193
228
  );
194
229
  }
195
230
  const resourceIds = new Set<string>();