@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.
- package/README.md +178 -2
- package/bin/converter-v5.js +2 -0
- package/dist/converter.min.cjs +162 -163
- package/dist/i3s-server/bin/i3s-server.min.cjs +73 -73
- package/dist/index.cjs +1749 -2152
- package/dist/index.js +1748 -2162
- package/dist/v5/adapters.cjs +1550 -128
- package/dist/v5/adapters.d.ts +16 -0
- package/dist/v5/adapters.js +1558 -117
- package/dist/v5/archive-source.d.ts +36 -0
- package/dist/v5/browser-tileset-source.d.ts +55 -11
- package/dist/v5/browser.cjs +18 -0
- package/dist/v5/browser.d.ts +14 -0
- package/dist/v5/browser.js +26 -0
- package/dist/v5/conversion-api.d.ts +9 -0
- package/dist/v5/converter-cli.d.ts +1 -0
- package/dist/v5/converter-cli.js +1214 -0
- package/dist/v5/core.cjs +471 -60
- package/dist/v5/core.d.ts +2 -0
- package/dist/v5/core.js +471 -60
- package/dist/v5/i3s-mesh-conversion.d.ts +4 -0
- package/dist/v5/i3s-selection-source.d.ts +70 -0
- package/dist/v5/index.cjs +10 -0
- package/dist/v5/index.d.ts +8 -0
- package/dist/v5/index.js +14 -0
- package/dist/v5/mesh-conversion.d.ts +18 -0
- package/dist/v5/mesh-hierarchy.d.ts +47 -0
- package/dist/v5/mesh-source-features.d.ts +2 -1
- package/dist/v5/mesh-source-schema.d.ts +12 -0
- package/dist/v5/mesh-source.d.ts +5 -2
- package/dist/v5/mesh-tileset-sink.d.ts +5 -0
- package/dist/v5/node-cli.d.ts +9 -0
- package/dist/v5/node-conversion.d.ts +32 -0
- package/dist/v5/node-feature-schema.d.ts +3 -0
- package/dist/v5/node-i3s-source.d.ts +41 -0
- package/dist/v5/node-source.d.ts +47 -0
- package/dist/v5/node.cjs +900 -0
- package/dist/v5/node.d.ts +6 -0
- package/dist/v5/node.js +877 -0
- package/dist/v5/point-cloud-file.d.ts +83 -0
- package/dist/v5/point-cloud-potree.d.ts +47 -0
- package/dist/v5/point-cloud-tileset.d.ts +44 -0
- package/dist/v5/resource-manifest.d.ts +10 -1
- package/dist/v5/selected-mesh-source.d.ts +53 -0
- package/package.json +33 -25
- package/src/index.ts +4 -1
- package/src/v4/3d-tiles-converter/helpers/load-i3s.ts +2 -4
- package/src/v4/converter-cli.ts +8 -2
- package/src/v4/i3s-converter/helpers/load-3d-tiles.ts +3 -51
- package/src/v4/i3s-converter/helpers/node-index-document.ts +5 -2
- package/src/v4/i3s-converter/helpers/preprocess-3d-tiles.ts +1 -1
- package/src/v4/i3s-converter/json-templates/store.ts +2 -1
- package/src/v5/adapters.ts +48 -0
- package/src/v5/archive-source.ts +235 -0
- package/src/v5/browser-tileset-source.ts +418 -48
- package/src/v5/browser.ts +45 -0
- package/src/v5/conversion-api.ts +69 -9
- package/src/v5/converter-cli.ts +28 -0
- package/src/v5/core.ts +6 -0
- package/src/v5/i3s-mesh-conversion.ts +40 -2
- package/src/v5/i3s-mesh-sink.ts +1 -1
- package/src/v5/i3s-mesh-source.ts +1 -0
- package/src/v5/i3s-selection-source.ts +298 -0
- package/src/v5/index.ts +26 -0
- package/src/v5/mesh-conversion.ts +20 -0
- package/src/v5/mesh-hierarchy.ts +648 -0
- package/src/v5/mesh-source-features.ts +39 -7
- package/src/v5/mesh-source-schema.ts +134 -0
- package/src/v5/mesh-source.ts +16 -4
- package/src/v5/mesh-tileset-sink.ts +1 -1
- package/src/v5/node-cli.ts +197 -0
- package/src/v5/node-conversion.ts +345 -0
- package/src/v5/node-feature-schema.ts +155 -0
- package/src/v5/node-i3s-source.ts +181 -0
- package/src/v5/node-source.ts +328 -0
- package/src/v5/node.ts +16 -0
- package/src/v5/point-cloud-file.ts +306 -0
- package/src/v5/point-cloud-potree.ts +130 -0
- package/src/v5/point-cloud-tileset.ts +137 -0
- package/src/v5/resource-fetcher.ts +4 -1
- package/src/v5/resource-manifest.ts +40 -5
- package/src/v5/selected-mesh-source.ts +384 -0
- 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
|
|
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 (
|
|
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
|
|
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 ?
|
|
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
|
-
|
|
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
|
+
}
|
package/src/v5/mesh-source.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 {
|
|
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
|
+
}
|