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

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 (83) hide show
  1. package/README.md +178 -2
  2. package/bin/converter-v5.js +2 -0
  3. package/dist/converter.min.cjs +162 -163
  4. package/dist/i3s-server/bin/i3s-server.min.cjs +73 -73
  5. package/dist/index.cjs +1749 -2152
  6. package/dist/index.js +1748 -2162
  7. package/dist/v5/adapters.cjs +1550 -128
  8. package/dist/v5/adapters.d.ts +16 -0
  9. package/dist/v5/adapters.js +1558 -117
  10. package/dist/v5/archive-source.d.ts +36 -0
  11. package/dist/v5/browser-tileset-source.d.ts +55 -11
  12. package/dist/v5/browser.cjs +18 -0
  13. package/dist/v5/browser.d.ts +14 -0
  14. package/dist/v5/browser.js +26 -0
  15. package/dist/v5/conversion-api.d.ts +9 -0
  16. package/dist/v5/converter-cli.d.ts +1 -0
  17. package/dist/v5/converter-cli.js +1214 -0
  18. package/dist/v5/core.cjs +471 -60
  19. package/dist/v5/core.d.ts +2 -0
  20. package/dist/v5/core.js +471 -60
  21. package/dist/v5/i3s-mesh-conversion.d.ts +4 -0
  22. package/dist/v5/i3s-selection-source.d.ts +70 -0
  23. package/dist/v5/index.cjs +10 -0
  24. package/dist/v5/index.d.ts +8 -0
  25. package/dist/v5/index.js +14 -0
  26. package/dist/v5/mesh-conversion.d.ts +18 -0
  27. package/dist/v5/mesh-hierarchy.d.ts +47 -0
  28. package/dist/v5/mesh-source-features.d.ts +2 -1
  29. package/dist/v5/mesh-source-schema.d.ts +12 -0
  30. package/dist/v5/mesh-source.d.ts +5 -2
  31. package/dist/v5/mesh-tileset-sink.d.ts +5 -0
  32. package/dist/v5/node-cli.d.ts +9 -0
  33. package/dist/v5/node-conversion.d.ts +32 -0
  34. package/dist/v5/node-feature-schema.d.ts +3 -0
  35. package/dist/v5/node-i3s-source.d.ts +41 -0
  36. package/dist/v5/node-source.d.ts +47 -0
  37. package/dist/v5/node.cjs +900 -0
  38. package/dist/v5/node.d.ts +6 -0
  39. package/dist/v5/node.js +877 -0
  40. package/dist/v5/point-cloud-file.d.ts +83 -0
  41. package/dist/v5/point-cloud-potree.d.ts +47 -0
  42. package/dist/v5/point-cloud-tileset.d.ts +44 -0
  43. package/dist/v5/resource-manifest.d.ts +10 -1
  44. package/dist/v5/selected-mesh-source.d.ts +53 -0
  45. package/package.json +33 -25
  46. package/src/index.ts +4 -1
  47. package/src/v4/3d-tiles-converter/helpers/load-i3s.ts +2 -4
  48. package/src/v4/converter-cli.ts +8 -2
  49. package/src/v4/i3s-converter/helpers/load-3d-tiles.ts +3 -51
  50. package/src/v4/i3s-converter/helpers/node-index-document.ts +5 -2
  51. package/src/v4/i3s-converter/helpers/preprocess-3d-tiles.ts +1 -1
  52. package/src/v4/i3s-converter/json-templates/store.ts +2 -1
  53. package/src/v5/adapters.ts +48 -0
  54. package/src/v5/archive-source.ts +235 -0
  55. package/src/v5/browser-tileset-source.ts +418 -48
  56. package/src/v5/browser.ts +45 -0
  57. package/src/v5/conversion-api.ts +69 -9
  58. package/src/v5/converter-cli.ts +28 -0
  59. package/src/v5/core.ts +6 -0
  60. package/src/v5/i3s-mesh-conversion.ts +40 -2
  61. package/src/v5/i3s-mesh-sink.ts +1 -1
  62. package/src/v5/i3s-mesh-source.ts +1 -0
  63. package/src/v5/i3s-selection-source.ts +298 -0
  64. package/src/v5/index.ts +26 -0
  65. package/src/v5/mesh-conversion.ts +20 -0
  66. package/src/v5/mesh-hierarchy.ts +648 -0
  67. package/src/v5/mesh-source-features.ts +39 -7
  68. package/src/v5/mesh-source-schema.ts +134 -0
  69. package/src/v5/mesh-source.ts +16 -4
  70. package/src/v5/mesh-tileset-sink.ts +1 -1
  71. package/src/v5/node-cli.ts +197 -0
  72. package/src/v5/node-conversion.ts +345 -0
  73. package/src/v5/node-feature-schema.ts +155 -0
  74. package/src/v5/node-i3s-source.ts +181 -0
  75. package/src/v5/node-source.ts +328 -0
  76. package/src/v5/node.ts +16 -0
  77. package/src/v5/point-cloud-file.ts +306 -0
  78. package/src/v5/point-cloud-potree.ts +130 -0
  79. package/src/v5/point-cloud-tileset.ts +137 -0
  80. package/src/v5/resource-fetcher.ts +4 -1
  81. package/src/v5/resource-manifest.ts +40 -5
  82. package/src/v5/selected-mesh-source.ts +384 -0
  83. package/src/v5/spatial-conversion.ts +88 -17
@@ -7,6 +7,8 @@ import type {GLTFPostprocessed, GLTFMeshPrimitivePostprocessed} from '@loaders.g
7
7
  import type {I3SMeshFeatures} from '@loaders.gl/i3s';
8
8
  import {convertFeatureAttributesToArrowBatches} from './feature-arrow.js';
9
9
  import type {FeatureArrowBatchOptions} from './feature-arrow.js';
10
+ import {createMeshSourceFeatureOptions} from './mesh-source-schema.js';
11
+ import type {MeshSourceSchemaOptions} from './mesh-source-schema.js';
10
12
  import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
11
13
 
12
14
  /** Explicit source feature mapping; target type changes must be authorized separately. */
@@ -29,7 +31,7 @@ export function extractMeshFeatures(
29
31
  featureTableJson?: Record<string, unknown>;
30
32
  },
31
33
  mesh: MeshGeometry,
32
- options?: MeshSourceFeatureOptions
34
+ options?: MeshSourceFeatureOptions | MeshSourceSchemaOptions
33
35
  ): I3SMeshFeatures | undefined {
34
36
  const declarations = primitive.extensions?.EXT_mesh_features?.featureIds;
35
37
  const metadata = gltf.extensions?.EXT_structural_metadata;
@@ -51,13 +53,14 @@ export function extractMeshFeatures(
51
53
  if (!options)
52
54
  throw new TileConversionError(
53
55
  'MESH_FEATURE_SCHEMA_REQUIRED',
54
- 'Feature extraction requires an explicit Arrow schema and class mapping'
56
+ 'Feature extraction requires a class selection and a declared Arrow or inline source schema'
55
57
  );
56
58
  if (!options.metadataClass)
57
59
  throw new TileConversionError(
58
60
  'MESH_FEATURE_CLASS_REQUIRED',
59
61
  'Feature metadata class must be explicit'
60
62
  );
63
+ const mapping = 'schema' in options ? options : createMeshSourceFeatureOptions(gltf, options);
61
64
  let count: number;
62
65
  let attribute = legacy;
63
66
  let properties: Record<string, ArrayLike<unknown>>;
@@ -94,6 +97,17 @@ export function extractMeshFeatures(
94
97
  'Feature class and declared table counts must agree'
95
98
  );
96
99
  count = table.count;
100
+ // Qualified mesh writers require every feature row to own geometry. Check before defaults allocate.
101
+ if (
102
+ !Number.isSafeInteger(count) ||
103
+ count <= 0 ||
104
+ count > mesh.attributes.POSITION.value.length / 3
105
+ ) {
106
+ throw new TileConversionError(
107
+ 'MESH_FEATURE_IDS_INVALID',
108
+ 'Feature row count exceeds the available mesh vertices'
109
+ );
110
+ }
97
111
  properties = {};
98
112
  const classProperties = metadata.schema?.classes?.[table.class]?.properties;
99
113
  if (!classProperties)
@@ -110,6 +124,7 @@ export function extractMeshFeatures(
110
124
  string,
111
125
  {
112
126
  type: string;
127
+ required?: boolean;
113
128
  componentType?: string;
114
129
  array?: boolean;
115
130
  noData?: unknown;
@@ -141,18 +156,35 @@ export function extractMeshFeatures(
141
156
  `Property ${name} requires a separately qualified semantic mapping`
142
157
  );
143
158
  const data = column?.data;
144
- if (data === undefined && property.default === undefined)
159
+ if (
160
+ (column !== undefined && (data === undefined || data === null)) ||
161
+ (data === undefined && property.required === true)
162
+ )
145
163
  throw new TileConversionError(
146
164
  'MESH_FEATURE_PROPERTY_UNAVAILABLE',
147
165
  `Property ${name} has not been decoded`
148
166
  );
149
- const decoded = data ?? Array.from({length: count}, () => property.default);
167
+ const integer64 = ['INT64', 'UINT64'].includes(property.componentType || '');
168
+ if (
169
+ integer64 &&
170
+ property.default !== undefined &&
171
+ (typeof property.default !== 'number' || !Number.isSafeInteger(property.default))
172
+ ) {
173
+ throw new TileConversionError(
174
+ 'MESH_FEATURE_PROPERTY_UNSUPPORTED',
175
+ `Property ${name} requires an exact 64-bit default`
176
+ );
177
+ }
178
+ const defaultValue =
179
+ integer64 && property.default !== undefined
180
+ ? BigInt(property.default as number)
181
+ : (property.default ?? null);
182
+ const decoded = data ?? Array.from({length: count}, () => defaultValue);
150
183
  if (property.noData === undefined || data === undefined) {
151
184
  properties[name] = decoded;
152
185
  continue;
153
186
  }
154
187
  const sentinel = property.noData;
155
- const integer64 = ['INT64', 'UINT64'].includes(property.componentType || '');
156
188
  if (
157
189
  (property.type === 'STRING'
158
190
  ? typeof sentinel !== 'string'
@@ -169,7 +201,7 @@ export function extractMeshFeatures(
169
201
  );
170
202
  const comparable = integer64 ? BigInt(sentinel as number) : sentinel;
171
203
  properties[name] = Array.from(decoded as ArrayLike<unknown>, value =>
172
- value === comparable ? (property.default ?? null) : value
204
+ value === comparable ? defaultValue : value
173
205
  );
174
206
  }
175
207
  } else {
@@ -237,7 +269,7 @@ export function extractMeshFeatures(
237
269
  .map(([name, values]) => [name, values[index]])
238
270
  )
239
271
  })),
240
- options
272
+ mapping
241
273
  );
242
274
  const indices = mesh.indices?.value;
243
275
  const triangleFeatureIndices = new Uint32Array((indices?.length ?? vertexCount) / 3);
@@ -0,0 +1,134 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {GLTFPostprocessed, GLTF_EXT_structural_metadata_ClassProperty} from '@loaders.gl/gltf';
6
+ import type {DataType, Field} from '@loaders.gl/schema';
7
+ import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
8
+ import type {MeshSourceFeatureOptions} from './mesh-source-features.js';
9
+
10
+ /** Class and identifier selection for a mapping derived from an inline glTF schema. */
11
+ export type MeshSourceSchemaOptions = Omit<MeshSourceFeatureOptions, 'schema' | 'rawMetadataField'>;
12
+
13
+ /** Declared scalar storage widths; no property values participate in type selection. */
14
+ const SCALAR_TYPES: Readonly<Record<string, DataType>> = {
15
+ INT8: 'int8',
16
+ UINT8: 'uint8',
17
+ INT16: 'int16',
18
+ UINT16: 'uint16',
19
+ INT32: 'int32',
20
+ UINT32: 'uint32',
21
+ INT64: 'int64',
22
+ UINT64: 'uint64',
23
+ FLOAT32: 'float32',
24
+ FLOAT64: 'float64'
25
+ };
26
+
27
+ /**
28
+ * Builds an explicit Arrow mapping from one inline structural-metadata class and property table.
29
+ * Scalar widths, strings and optional/default/noData declarations are retained. The caller selects
30
+ * the class and stable identifier property; otherwise identifiers are uint32 table row indices.
31
+ * Arrays, enums, vectors, transformed numeric properties and external schemas fail explicitly.
32
+ * This does not authorize destination type changes or loss of source class semantics.
33
+ */
34
+ export function createMeshSourceFeatureOptions(
35
+ gltf: GLTFPostprocessed,
36
+ options: MeshSourceSchemaOptions
37
+ ): MeshSourceFeatureOptions {
38
+ const metadata = gltf.extensions?.EXT_structural_metadata;
39
+ const table = metadata?.propertyTables?.[0];
40
+ const properties = metadata?.schema?.classes?.[options.metadataClass]?.properties;
41
+ if (
42
+ !options.metadataClass ||
43
+ metadata?.propertyTables?.length !== 1 ||
44
+ table?.class !== options.metadataClass ||
45
+ !properties ||
46
+ !Number.isSafeInteger(table.count) ||
47
+ table.count < 1 ||
48
+ table.count > 0xffffffff ||
49
+ metadata.propertyTextures?.length ||
50
+ metadata.propertyAttributes?.length
51
+ ) {
52
+ throw new TileConversionError(
53
+ 'MESH_FEATURE_SCHEMA_REQUIRED',
54
+ 'Schema mapping requires one inline class, one matching property table and a positive uint32 row count'
55
+ );
56
+ }
57
+ if (Object.keys(table.properties || {}).some(name => !Object.hasOwn(properties, name))) {
58
+ throw new TileConversionError(
59
+ 'MESH_FEATURE_PROPERTY_UNMAPPED',
60
+ 'Property table columns must be declared in the selected class'
61
+ );
62
+ }
63
+ const fields: Field[] = Object.entries(properties).map(([name, property]) => {
64
+ const declaration = property as GLTF_EXT_structural_metadata_ClassProperty;
65
+ const column = table.properties?.[name];
66
+ if (
67
+ declaration.array ||
68
+ !['SCALAR', 'STRING'].includes(declaration.type) ||
69
+ declaration.normalized ||
70
+ declaration.offset !== undefined ||
71
+ declaration.scale !== undefined ||
72
+ column?.offset !== undefined ||
73
+ column?.scale !== undefined ||
74
+ (declaration.required &&
75
+ (declaration.noData !== undefined || declaration.default !== undefined))
76
+ ) {
77
+ throw new TileConversionError(
78
+ 'MESH_FEATURE_PROPERTY_UNSUPPORTED',
79
+ `Property ${name} requires an explicit separately qualified mapping`
80
+ );
81
+ }
82
+ const type =
83
+ declaration.type === 'STRING'
84
+ ? 'utf8'
85
+ : Object.hasOwn(SCALAR_TYPES, declaration.componentType || '')
86
+ ? SCALAR_TYPES[declaration.componentType!]
87
+ : undefined;
88
+ if (!type) {
89
+ throw new TileConversionError(
90
+ 'MESH_FEATURE_PROPERTY_UNSUPPORTED',
91
+ `Property ${name} has no supported declared scalar width`
92
+ );
93
+ }
94
+ return {
95
+ name,
96
+ type,
97
+ nullable: declaration.required !== true,
98
+ metadata: {'loaders.gl:structural-property': JSON.stringify(declaration)}
99
+ };
100
+ });
101
+ const identifierName = options.featureIdField || 'feature_id';
102
+ const sourceIdentifier = options.sourceFeatureIdProperty;
103
+ const sourceField = sourceIdentifier
104
+ ? fields.find(field => field.name === sourceIdentifier)
105
+ : undefined;
106
+ if (
107
+ (sourceIdentifier && !sourceField) ||
108
+ fields.some(field => field.name === identifierName && field !== sourceField) ||
109
+ (sourceField &&
110
+ !['utf8', 'int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32', 'int64', 'uint64'].includes(
111
+ String(sourceField.type)
112
+ ))
113
+ ) {
114
+ throw new TileConversionError(
115
+ 'MESH_FEATURE_ID_PROPERTY_REQUIRED',
116
+ 'Select a declared string/integer identifier and a distinct output identifier name'
117
+ );
118
+ }
119
+ return {
120
+ ...options,
121
+ schema: {
122
+ fields: [
123
+ {
124
+ ...sourceField,
125
+ name: identifierName,
126
+ type: sourceField?.type || 'uint32',
127
+ nullable: false
128
+ },
129
+ ...fields.filter(field => field !== sourceField)
130
+ ],
131
+ metadata: {'loaders.gl:source-schema': JSON.stringify(metadata.schema)}
132
+ }
133
+ };
134
+ }
@@ -21,6 +21,7 @@ import {validateMeshGeometry, validateMeshIndices} from './mesh.js';
21
21
  import {resolveMeshSourceTexture} from './mesh-source-texture.js';
22
22
  import {extractMeshFeatures} from './mesh-source-features.js';
23
23
  import type {MeshSourceFeatureOptions} from './mesh-source-features.js';
24
+ import type {MeshSourceSchemaOptions} from './mesh-source-schema.js';
24
25
  import type {I3SMeshFeatures} from '@loaders.gl/i3s';
25
26
  export type {MeshSourceFeatureOptions} from './mesh-source-features.js';
26
27
 
@@ -28,12 +29,14 @@ export type {MeshSourceFeatureOptions} from './mesh-source-features.js';
28
29
  export interface MeshSourceResource extends MeshConversionInput {
29
30
  /** Arrow features associated with the selected primitive; IDs are distinct from table row indices. */
30
31
  readonly features?: I3SMeshFeatures;
32
+ /** Original I3S layer semantics, retained for destination policy and diagnostics. */
33
+ readonly sourceLayerType?: '3DObject' | 'IntegratedMesh';
31
34
  }
32
35
 
33
36
  /** Narrow source profile and content lifetime controls. */
34
37
  export interface MeshTilesetSourceOptions extends TilesetConversionSourceOptions {
35
- /** Required when the source has batch/structural metadata; no schema is inferred. */
36
- readonly features?: MeshSourceFeatureOptions;
38
+ /** Required for feature-bearing content; omit schema only to map an inline structural metadata class. */
39
+ readonly features?: MeshSourceFeatureOptions | MeshSourceSchemaOptions;
37
40
  /** Explicit bounded reader for external encoded images, relative to the content URI. */
38
41
  readonly readExternalResource?: (
39
42
  uri: string,
@@ -172,7 +175,11 @@ export function createMeshTilesetConversionSource(
172
175
  'A default scene or exactly one scene is required'
173
176
  );
174
177
  let primitiveIndex = 0;
175
- for (const selected of traverseMeshNodes(roots, placement)) {
178
+ const primitives = traverseMeshNodes(roots, placement);
179
+ let current = primitives.next();
180
+ while (!current.done) {
181
+ const selected = current.value;
182
+ const next = primitives.next();
176
183
  const resource = await extractPrimitive(
177
184
  selected.primitive,
178
185
  selected.transform,
@@ -188,7 +195,12 @@ export function createMeshTilesetConversionSource(
188
195
  resource.mesh,
189
196
  options.features
190
197
  );
191
- yield {...resource, features};
198
+ yield {
199
+ ...resource,
200
+ features,
201
+ contentPrimitive: {index: primitiveIndex - 1, last: Boolean(next.done)}
202
+ };
203
+ current = next;
192
204
  }
193
205
  if (!primitiveIndex)
194
206
  throw new TileConversionError(
@@ -96,7 +96,7 @@ export function createSingleMeshTilesetSink(
96
96
  }
97
97
 
98
98
  /** Builds placement and local bounds for a trusted single-mesh codec output. */
99
- function createMeshTileset(
99
+ export function createMeshTileset(
100
100
  resource: EncodedMeshConversionResource,
101
101
  geometricError: number
102
102
  ): Omit<Tiles3DTilesetJSON, 'root'> & {root: Omit<Tiles3DTileJSON, 'children'>} {
@@ -0,0 +1,197 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {parseArgs} from 'node:util';
6
+ import {readFeatureSchema} from './node-feature-schema.js';
7
+ import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
8
+ import {createNodeTilesetConversionSource, NODE_TILESET_LIMITS} from './node-source.js';
9
+ import type {NodeTileConversionOutputFormat} from './node-conversion.js';
10
+
11
+ /** Text destinations supplied by the process wrapper or a hermetic caller. */
12
+ export interface TileConverterCLIOutput {
13
+ /** Writes a complete JSON result or help text. */
14
+ readonly stdout: (text: string) => void;
15
+ /** Writes a JSON error without contaminating stdout. */
16
+ readonly stderr: (text: string) => void;
17
+ }
18
+
19
+ /** Usage for bounded metadata inspection and selected static mesh conversion. */
20
+ const HELP = `Usage: tile-converter-v5 inspect|convert|validate <input path or URL> [options]
21
+
22
+ --input-format 3d-tiles|3tz|i3s|slpk Explicit input format (default: 3d-tiles)
23
+ --geometric-error N Required source error in meters for I3S to 3D Tiles
24
+ --integrated-mesh-policy as-3d-object Authorize I3S layer-type change
25
+ --preserve-hierarchy Retain a complete explicit 3D Tiles tree (3D Tiles/3TZ only)
26
+ --feature-schema PATH Local JSON feature class/schema mapping (at most 64 KiB)
27
+ --resource ID Selected content placement; repeat for multiple selections
28
+ --output-format 3d-tiles|3tz|slpk Target profile (default: 3tz)
29
+ --output PATH New destination (required for convert, forbidden for validate)
30
+ --max-input-bytes N Lower the 16 MiB input/read budget
31
+ --max-resources N Lower the 1,000 content-declaration limit
32
+ --max-documents N Lower the 32 metadata-document limit
33
+ --max-output-bytes N Lower the 32 MiB output/archive budget
34
+ --help Show this help
35
+
36
+ Inspect reads metadata only, following declared .json external tilesets.
37
+ Convert/validate require explicit selections; I3S uses declared CRS and shared spatial preparation.
38
+ Validate decodes, converts and packages the selected target profile without writing files.
39
+ Output defaults to a partial flat mesh collection. --preserve-hierarchy requires every declared content.
40
+ Remote archives require HTTP 206 ranges and a stable ETag or Last-Modified validator.
41
+ I3S inspect lists leaf nodes; feature-bearing conversion requires --feature-schema.
42
+ Hierarchy mode supports at most 32 document placements, 256 tiles and 64 total mesh primitive placements, including ordered 1.1 contents arrays.
43
+ `;
44
+
45
+ /** Runs a bounded v5 operation, returning an exit code after source/output cleanup. */
46
+ export async function runTileConverterCLI(
47
+ argumentsList: string[],
48
+ output: TileConverterCLIOutput,
49
+ signal?: AbortSignal
50
+ ): Promise<number> {
51
+ try {
52
+ const {values, positionals} = parseArgs({
53
+ args: argumentsList,
54
+ allowPositionals: true,
55
+ options: {
56
+ help: {type: 'boolean'},
57
+ 'preserve-hierarchy': {type: 'boolean'},
58
+ 'input-format': {type: 'string', default: '3d-tiles'},
59
+ 'max-input-bytes': {type: 'string', default: String(NODE_TILESET_LIMITS.maxInputBytes)},
60
+ 'max-resources': {type: 'string', default: String(NODE_TILESET_LIMITS.maxInputResources)},
61
+ 'max-documents': {type: 'string', default: String(NODE_TILESET_LIMITS.maxTilesetDocuments)},
62
+ 'max-output-bytes': {type: 'string', default: String(32 * 1024 * 1024)},
63
+ 'output-format': {type: 'string', default: '3tz'},
64
+ output: {type: 'string'},
65
+ resource: {type: 'string', multiple: true},
66
+ 'feature-schema': {type: 'string'},
67
+ 'geometric-error': {type: 'string'},
68
+ 'integrated-mesh-policy': {type: 'string'}
69
+ }
70
+ });
71
+ if (values.help || !argumentsList.length) {
72
+ output.stdout(HELP);
73
+ return 0;
74
+ }
75
+ if (
76
+ positionals.length !== 2 ||
77
+ !['inspect', 'convert', 'validate'].includes(positionals[0]) ||
78
+ !['3d-tiles', '3tz', 'i3s', 'slpk'].includes(values['input-format'])
79
+ )
80
+ throw new TileConversionError(
81
+ 'CLI_USAGE_ERROR',
82
+ 'Use inspect, convert or validate with one input and --input-format 3d-tiles, 3tz, i3s or slpk'
83
+ );
84
+ const format = values['input-format'] as '3d-tiles' | '3tz' | 'i3s' | 'slpk';
85
+ const operation = positionals[0];
86
+ const sourceOptions = {
87
+ input: positionals[1],
88
+ format,
89
+ signal,
90
+ maxInputBytes: parseLimit(values['max-input-bytes']),
91
+ maxInputResources: parseLimit(values['max-resources']),
92
+ maxTilesetDocuments: parseLimit(values['max-documents']),
93
+ inspectNestedTilesets: true
94
+ };
95
+ if (operation !== 'inspect') {
96
+ if (
97
+ !values.resource?.length ||
98
+ !['3d-tiles', '3tz', 'slpk'].includes(values['output-format']) ||
99
+ (operation === 'convert' ? !values.output : values.output !== undefined)
100
+ )
101
+ throw new TileConversionError(
102
+ 'CLI_USAGE_ERROR',
103
+ 'Convert/validate require --resource selections and a supported --output-format; only convert requires --output'
104
+ );
105
+ if (
106
+ values['integrated-mesh-policy'] !== undefined &&
107
+ values['integrated-mesh-policy'] !== 'as-3d-object'
108
+ )
109
+ throw new TileConversionError(
110
+ 'CLI_USAGE_ERROR',
111
+ 'Use --integrated-mesh-policy as-3d-object'
112
+ );
113
+ const features =
114
+ values['feature-schema'] === undefined
115
+ ? undefined
116
+ : await readFeatureSchema(
117
+ values['feature-schema'],
118
+ format === 'i3s' || format === 'slpk',
119
+ signal
120
+ );
121
+ const {convertNodeTileset} = await import('./node-conversion.js');
122
+ const report = await convertNodeTileset({
123
+ ...sourceOptions,
124
+ ...features,
125
+ resourceIds: values.resource,
126
+ preserveHierarchy: values['preserve-hierarchy'],
127
+ outputFormat: values['output-format'] as NodeTileConversionOutputFormat,
128
+ output: values.output,
129
+ maxOutputBytes: parseLimit(values['max-output-bytes']),
130
+ geometricError:
131
+ values['geometric-error'] === undefined
132
+ ? undefined
133
+ : parseGeometricError(values['geometric-error']),
134
+ integratedMeshPolicy: values['integrated-mesh-policy'] as 'as-3d-object' | undefined
135
+ });
136
+ output.stdout(
137
+ `${JSON.stringify({operation, input: positionals[1], inputFormat: format, outputFormat: values['output-format'], ...(values.output ? {output: values.output} : {}), report}, null, 2)}\n`
138
+ );
139
+ return 0;
140
+ }
141
+ if (
142
+ values['feature-schema'] !== undefined ||
143
+ values['preserve-hierarchy'] !== undefined ||
144
+ values['geometric-error'] !== undefined ||
145
+ values['integrated-mesh-policy'] !== undefined ||
146
+ values.resource ||
147
+ values.output ||
148
+ values['output-format'] !== '3tz' ||
149
+ values['max-output-bytes'] !== String(32 * 1024 * 1024)
150
+ )
151
+ throw new TileConversionError(
152
+ 'CLI_USAGE_ERROR',
153
+ 'Inspect does not accept conversion/output options'
154
+ );
155
+ const source =
156
+ format === 'i3s' || format === 'slpk'
157
+ ? await (await import('./node-i3s-source.js')).createNodeI3SConversionSource({
158
+ ...sourceOptions,
159
+ format
160
+ })
161
+ : await createNodeTilesetConversionSource({...sourceOptions, format});
162
+ const inspection = await source.inspect(signal).finally(() => source.close());
163
+ output.stdout(
164
+ `${JSON.stringify({operation: 'inspect', input: positionals[1], inputFormat: format, ...inspection}, null, 2)}\n`
165
+ );
166
+ return 0;
167
+ } catch (error) {
168
+ output.stderr(
169
+ `${JSON.stringify({
170
+ code: error instanceof Error && 'code' in error ? error.code : 'CONVERSION_FAILED',
171
+ message: error instanceof Error ? error.message : String(error),
172
+ diagnostics: error instanceof TileConversionError ? error.diagnostics : []
173
+ })}\n`
174
+ );
175
+ return 1;
176
+ }
177
+ }
178
+
179
+ /** Parses a positive decimal byte/count limit without accepting fractional or infinite values. */
180
+ function parseLimit(value: string): number {
181
+ if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value)))
182
+ throw new TileConversionError(
183
+ 'CLI_USAGE_ERROR',
184
+ 'Limits must be positive safe decimal integers'
185
+ );
186
+ return Number(value);
187
+ }
188
+
189
+ /** Parses an explicit metric source error without inferring I3S screen-size semantics. */
190
+ function parseGeometricError(value: string): number {
191
+ if (!/^(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(value) || !Number.isFinite(Number(value)))
192
+ throw new TileConversionError(
193
+ 'CLI_USAGE_ERROR',
194
+ 'Geometric error must be a finite nonnegative number in meters'
195
+ );
196
+ return Number(value);
197
+ }