@loaders.gl/tile-converter 5.0.0-alpha.10 → 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 +211 -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 +1933 -2232
- package/dist/index.js +1925 -2235
- package/dist/v5/adapters.cjs +3186 -208
- package/dist/v5/adapters.d.ts +29 -2
- package/dist/v5/adapters.js +3202 -196
- package/dist/v5/archive-source.d.ts +36 -0
- package/dist/v5/browser-tileset-source.d.ts +55 -11
- package/dist/v5/browser.cjs +30 -0
- package/dist/v5/browser.d.ts +26 -2
- package/dist/v5/browser.js +45 -1
- 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 +515 -50
- package/dist/v5/core.d.ts +2 -0
- package/dist/v5/core.js +515 -50
- package/dist/v5/i3s-mesh-conversion.d.ts +14 -4
- package/dist/v5/i3s-mesh-sink.d.ts +25 -0
- package/dist/v5/i3s-mesh-source-features.d.ts +13 -0
- package/dist/v5/i3s-mesh-source-material.d.ts +5 -0
- package/dist/v5/i3s-mesh-source.d.ts +30 -0
- package/dist/v5/i3s-selection-source.d.ts +70 -0
- package/dist/v5/index.cjs +20 -0
- package/dist/v5/index.d.ts +18 -2
- package/dist/v5/index.js +31 -1
- package/dist/v5/mesh-conversion.d.ts +29 -2
- package/dist/v5/mesh-features.d.ts +9 -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-texture.d.ts +2 -0
- package/dist/v5/mesh-source.d.ts +13 -3
- package/dist/v5/mesh-tileset-sink.d.ts +5 -0
- package/dist/v5/mesh.d.ts +6 -1
- 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-source-encoder.d.ts +4 -7
- package/dist/v5/point-cloud-spatial.d.ts +22 -0
- package/dist/v5/point-cloud-tileset-sink.d.ts +28 -0
- package/dist/v5/point-cloud-tileset.d.ts +44 -0
- package/dist/v5/point-cloud.d.ts +26 -1
- package/dist/v5/resource-fetcher.d.ts +18 -0
- package/dist/v5/resource-manifest.d.ts +10 -1
- package/dist/v5/selected-mesh-source.d.ts +53 -0
- package/dist/v5/spatial-conversion.d.ts +3 -1
- 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 +69 -2
- package/src/v5/archive-source.ts +235 -0
- package/src/v5/browser-tileset-source.ts +418 -48
- package/src/v5/browser.ts +81 -2
- 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 +97 -19
- package/src/v5/i3s-mesh-sink.ts +351 -0
- package/src/v5/i3s-mesh-source-features.ts +279 -0
- package/src/v5/i3s-mesh-source-material.ts +157 -0
- package/src/v5/i3s-mesh-source.ts +228 -0
- package/src/v5/i3s-selection-source.ts +298 -0
- package/src/v5/index.ts +60 -2
- package/src/v5/mesh-conversion.ts +61 -11
- package/src/v5/mesh-draco.ts +47 -3
- package/src/v5/mesh-features.ts +319 -0
- package/src/v5/mesh-hierarchy.ts +648 -0
- package/src/v5/mesh-source-features.ts +73 -10
- package/src/v5/mesh-source-schema.ts +134 -0
- package/src/v5/mesh-source-texture.ts +47 -0
- package/src/v5/mesh-source.ts +99 -25
- package/src/v5/mesh-tileset-sink.ts +1 -1
- package/src/v5/mesh.ts +16 -7
- 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-conversion.ts +9 -1
- package/src/v5/point-cloud-file.ts +306 -0
- package/src/v5/point-cloud-potree.ts +130 -0
- package/src/v5/point-cloud-source-encoder.ts +31 -20
- package/src/v5/point-cloud-spatial.ts +280 -0
- package/src/v5/point-cloud-tileset-sink.ts +234 -0
- package/src/v5/point-cloud-tileset.ts +137 -0
- package/src/v5/point-cloud.ts +159 -32
- package/src/v5/resource-fetcher.ts +113 -0
- package/src/v5/resource-manifest.ts +40 -5
- package/src/v5/selected-mesh-source.ts +384 -0
- package/src/v5/spatial-conversion.ts +161 -6
package/src/v5/index.ts
CHANGED
|
@@ -30,8 +30,14 @@ export type {
|
|
|
30
30
|
FeatureArrowBatchOptions,
|
|
31
31
|
TileFeatureAttributes
|
|
32
32
|
} from '@loaders.gl/tile-converter/v5/adapters';
|
|
33
|
-
export {
|
|
34
|
-
|
|
33
|
+
export {
|
|
34
|
+
encodePointCloudTile,
|
|
35
|
+
encodePointCloudTileWithMetadata
|
|
36
|
+
} from '@loaders.gl/tile-converter/v5/adapters';
|
|
37
|
+
export type {
|
|
38
|
+
EncodePointCloudTileOptions,
|
|
39
|
+
EncodedPointCloudTile
|
|
40
|
+
} from '@loaders.gl/tile-converter/v5/adapters';
|
|
35
41
|
export {traversePointCloudSource} from '@loaders.gl/tile-converter/v5/core';
|
|
36
42
|
export type {
|
|
37
43
|
PointCloudSourceTile,
|
|
@@ -97,3 +103,55 @@ export type {
|
|
|
97
103
|
I3SMeshConversionCodecOptions,
|
|
98
104
|
SingleMeshI3SSink
|
|
99
105
|
} from '@loaders.gl/tile-converter/v5/adapters';
|
|
106
|
+
|
|
107
|
+
export {
|
|
108
|
+
createI3SMeshSink,
|
|
109
|
+
createTileConversionResourceFetcher
|
|
110
|
+
} from '@loaders.gl/tile-converter/v5/adapters';
|
|
111
|
+
export type {
|
|
112
|
+
I3SMeshSink,
|
|
113
|
+
I3SMeshSinkOptions,
|
|
114
|
+
TileConversionResourceFetcherOptions,
|
|
115
|
+
MeshTileFeatures
|
|
116
|
+
} from '@loaders.gl/tile-converter/v5/adapters';
|
|
117
|
+
|
|
118
|
+
export {createPointCloudTilesetSink} from '@loaders.gl/tile-converter/v5/adapters';
|
|
119
|
+
export type {
|
|
120
|
+
PointCloudTilesetSink,
|
|
121
|
+
PointCloudTilesetSinkOptions
|
|
122
|
+
} from '@loaders.gl/tile-converter/v5/adapters';
|
|
123
|
+
|
|
124
|
+
export {transformPointCloudSourceTile} from '@loaders.gl/tile-converter/v5/adapters';
|
|
125
|
+
export type {PointCloudSpatialOptions} from '@loaders.gl/tile-converter/v5/adapters';
|
|
126
|
+
|
|
127
|
+
export {createI3SMeshTilesetConversionSource} from '@loaders.gl/tile-converter/v5/adapters';
|
|
128
|
+
export type {
|
|
129
|
+
I3SMeshTilesetSourceOptions,
|
|
130
|
+
I3SMeshSourceFeatureOptions
|
|
131
|
+
} from '@loaders.gl/tile-converter/v5/adapters';
|
|
132
|
+
|
|
133
|
+
export {convertPointCloudToTileset} from '@loaders.gl/tile-converter/v5/adapters';
|
|
134
|
+
export type {
|
|
135
|
+
ConvertPointCloudToTilesetOptions,
|
|
136
|
+
ConvertedPointCloudTileset
|
|
137
|
+
} from '@loaders.gl/tile-converter/v5/adapters';
|
|
138
|
+
|
|
139
|
+
export {
|
|
140
|
+
encodePotreeArchive,
|
|
141
|
+
openPointCloudFile,
|
|
142
|
+
extractMeshVertices
|
|
143
|
+
} from '@loaders.gl/tile-converter/v5/adapters';
|
|
144
|
+
export type {
|
|
145
|
+
EncodePotreeArchiveOptions,
|
|
146
|
+
OpenPointCloudFileOptions,
|
|
147
|
+
OpenedPointCloudFile,
|
|
148
|
+
ExtractMeshVerticesOptions
|
|
149
|
+
} from '@loaders.gl/tile-converter/v5/adapters';
|
|
150
|
+
|
|
151
|
+
export {createMeshSourceFeatureOptions} from '@loaders.gl/tile-converter/v5/adapters';
|
|
152
|
+
export type {MeshSourceSchemaOptions} from '@loaders.gl/tile-converter/v5/adapters';
|
|
153
|
+
export {convertPointCloudToPotree} from '@loaders.gl/tile-converter/v5/adapters';
|
|
154
|
+
export type {
|
|
155
|
+
ConvertPointCloudToPotreeOptions,
|
|
156
|
+
ConvertedPointCloudPotree
|
|
157
|
+
} from '@loaders.gl/tile-converter/v5/adapters';
|
|
@@ -8,8 +8,12 @@ import type {MeshGeometry} from '@loaders.gl/schema';
|
|
|
8
8
|
import type {LoadLibraryOptions} from '@loaders.gl/worker-utils';
|
|
9
9
|
import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
|
|
10
10
|
import type {TileConversionCodec} from '@loaders.gl/tile-converter/v5/core';
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
Tiles3DConversionSpatialContext,
|
|
13
|
+
I3SConversionSpatialContext
|
|
14
|
+
} from '@loaders.gl/tile-converter/v5/core';
|
|
12
15
|
import {encodeMeshTile, validateMeshGeometry} from './mesh.js';
|
|
16
|
+
import type {MeshTileFeatures} from './mesh-features.js';
|
|
13
17
|
import type {MeshTileMaterial} from './mesh.js';
|
|
14
18
|
import {encodeDracoMeshTile} from './mesh-draco.js';
|
|
15
19
|
|
|
@@ -17,10 +21,23 @@ import {encodeDracoMeshTile} from './mesh-draco.js';
|
|
|
17
21
|
export interface MeshConversionInput {
|
|
18
22
|
/** Caller-owned resource identifier, forwarded to the output sink. */
|
|
19
23
|
readonly id: string;
|
|
24
|
+
/** Inspected content placement, used by the hierarchy sink independently of primitive IDs. */
|
|
25
|
+
readonly contentResourceId?: string;
|
|
26
|
+
/** Ordered primitive within a content; last marks complete extraction for hierarchy authoring. */
|
|
27
|
+
readonly contentPrimitive?: {
|
|
28
|
+
/** Zero-based primitive placement index in scene traversal order. */
|
|
29
|
+
readonly index: number;
|
|
30
|
+
/** Whether this is the final primitive placement in the content. */
|
|
31
|
+
readonly last: boolean;
|
|
32
|
+
};
|
|
20
33
|
/** Triangle geometry with absolute packed Float32 or Float64 source positions. */
|
|
21
34
|
readonly mesh: MeshGeometry;
|
|
35
|
+
/** Original I3S vector basis; overrides the codec default when supplied by a source. */
|
|
36
|
+
readonly normalReferenceFrame?: 'earth-centered' | 'vertex-reference-frame';
|
|
22
37
|
/** Optional single material with an embedded base-color image, mapped explicitly by the source adapter. */
|
|
23
38
|
readonly material?: MeshTileMaterial;
|
|
39
|
+
/** Explicit feature table and triangle associations, written as structural metadata. */
|
|
40
|
+
readonly features?: MeshTileFeatures;
|
|
24
41
|
/** Finite xyz origin in the target frame, subtracted before float32 encoding. */
|
|
25
42
|
readonly origin: readonly [number, number, number];
|
|
26
43
|
}
|
|
@@ -29,6 +46,15 @@ export interface MeshConversionInput {
|
|
|
29
46
|
export interface EncodedMeshConversionResource {
|
|
30
47
|
/** Caller-owned resource identifier. */
|
|
31
48
|
readonly id: string;
|
|
49
|
+
/** Inspected content placement, retained by the codec for hierarchy authoring. */
|
|
50
|
+
readonly contentResourceId?: string;
|
|
51
|
+
/** Ordered primitive within a content; last marks complete extraction for hierarchy authoring. */
|
|
52
|
+
readonly contentPrimitive?: {
|
|
53
|
+
/** Zero-based primitive placement index in scene traversal order. */
|
|
54
|
+
readonly index: number;
|
|
55
|
+
/** Whether this is the final primitive placement in the content. */
|
|
56
|
+
readonly last: boolean;
|
|
57
|
+
};
|
|
32
58
|
/** Self-contained GLB with local positions and transformed normals. */
|
|
33
59
|
readonly glb: ArrayBuffer;
|
|
34
60
|
/** Target-frame translation to add to the encoded positions. */
|
|
@@ -46,7 +72,11 @@ export interface EncodedMeshConversionResource {
|
|
|
46
72
|
/** Spatial policy used by the concrete mesh conversion codec. */
|
|
47
73
|
export interface MeshConversionCodecOptions {
|
|
48
74
|
/** Shared context that accepts absolute source positions and returns absolute target positions. */
|
|
49
|
-
readonly spatialContext: Tiles3DConversionSpatialContext;
|
|
75
|
+
readonly spatialContext: Tiles3DConversionSpatialContext | I3SConversionSpatialContext;
|
|
76
|
+
/** I3S vector frame when using an I3S context; defaults to earth-centered. */
|
|
77
|
+
readonly normalReferenceFrame?: 'earth-centered' | 'vertex-reference-frame';
|
|
78
|
+
/** Rebase around the transformed bounds center instead of the resource's explicit origin. */
|
|
79
|
+
readonly autoOrigin?: boolean;
|
|
50
80
|
/** Maximum Euclidean reconstruction error per vertex, in target coordinate units. */
|
|
51
81
|
readonly maxPositionError: number;
|
|
52
82
|
/** Lossless Draco Edge Breaker is enabled by default; false emits uncompressed GLB geometry. */
|
|
@@ -105,12 +135,6 @@ export function createMeshConversionCodec<TInspection = unknown>(
|
|
|
105
135
|
/** Prepares and encodes one geometry while honoring cancellation. */
|
|
106
136
|
async *convert(resource, _inspection, signal) {
|
|
107
137
|
signal?.throwIfAborted();
|
|
108
|
-
if ('features' in resource && resource.features) {
|
|
109
|
-
throw new TileConversionError(
|
|
110
|
-
'MESH_FEATURE_OUTPUT_UNSUPPORTED',
|
|
111
|
-
'The GLB codec requires an explicit feature metadata writer; use the I3S codec for supported feature-bearing meshes'
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
138
|
const geometry = validateMeshGeometry(resource.mesh, true);
|
|
115
139
|
const sourcePositions = geometry.attributes.POSITION.value;
|
|
116
140
|
const origin: [number, number, number] = [...resource.origin];
|
|
@@ -120,7 +144,21 @@ export function createMeshConversionCodec<TInspection = unknown>(
|
|
|
120
144
|
'Target origin must contain three finite coordinates'
|
|
121
145
|
);
|
|
122
146
|
}
|
|
123
|
-
const
|
|
147
|
+
const transformed =
|
|
148
|
+
'transformGeometryAsync' in spatialContext
|
|
149
|
+
? await spatialContext.transformGeometryAsync(
|
|
150
|
+
sourcePositions,
|
|
151
|
+
geometry.attributes.NORMAL?.value,
|
|
152
|
+
resource.normalReferenceFrame ?? options.normalReferenceFrame
|
|
153
|
+
)
|
|
154
|
+
: {
|
|
155
|
+
positions: spatialContext.transformPositions(sourcePositions),
|
|
156
|
+
normals: geometry.attributes.NORMAL
|
|
157
|
+
? spatialContext.transformNormals(geometry.attributes.NORMAL.value, sourcePositions)
|
|
158
|
+
: undefined
|
|
159
|
+
};
|
|
160
|
+
signal?.throwIfAborted();
|
|
161
|
+
const targetPositions = transformed.positions;
|
|
124
162
|
if (
|
|
125
163
|
targetPositions.length !== sourcePositions.length ||
|
|
126
164
|
targetPositions.some(value => !Number.isFinite(value))
|
|
@@ -130,6 +168,16 @@ export function createMeshConversionCodec<TInspection = unknown>(
|
|
|
130
168
|
'Spatial transformation must return one finite xyz position per vertex'
|
|
131
169
|
);
|
|
132
170
|
}
|
|
171
|
+
if (options.autoOrigin) {
|
|
172
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
173
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
174
|
+
for (let index = 0; index < targetPositions.length; index++) {
|
|
175
|
+
const axis = index % 3;
|
|
176
|
+
minimum[axis] = Math.min(minimum[axis], targetPositions[index]);
|
|
177
|
+
maximum[axis] = Math.max(maximum[axis], targetPositions[index]);
|
|
178
|
+
}
|
|
179
|
+
for (let axis = 0; axis < 3; axis++) origin[axis] = minimum[axis] / 2 + maximum[axis] / 2;
|
|
180
|
+
}
|
|
133
181
|
const localPositions = new Float32Array(targetPositions.length);
|
|
134
182
|
const minimum = [Infinity, Infinity, Infinity];
|
|
135
183
|
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
@@ -156,11 +204,11 @@ export function createMeshConversionCodec<TInspection = unknown>(
|
|
|
156
204
|
geometry.attributes.POSITION = {value: localPositions, size: 3};
|
|
157
205
|
if (geometry.attributes.NORMAL) {
|
|
158
206
|
geometry.attributes.NORMAL = {
|
|
159
|
-
value:
|
|
207
|
+
value: transformed.normals!,
|
|
160
208
|
size: 3
|
|
161
209
|
};
|
|
162
210
|
}
|
|
163
|
-
const appearance = {material: resource.material};
|
|
211
|
+
const appearance = {material: resource.material, features: resource.features};
|
|
164
212
|
const encoded = draco
|
|
165
213
|
? await encodeDracoMeshTile(geometry, appearance, dracoLibraryOptions)
|
|
166
214
|
: {glb: encodeMeshTile(geometry, appearance), positions: localPositions};
|
|
@@ -175,6 +223,8 @@ export function createMeshConversionCodec<TInspection = unknown>(
|
|
|
175
223
|
}
|
|
176
224
|
yield {
|
|
177
225
|
id: resource.id,
|
|
226
|
+
contentResourceId: resource.contentResourceId,
|
|
227
|
+
contentPrimitive: resource.contentPrimitive,
|
|
178
228
|
glb: encoded.glb,
|
|
179
229
|
origin,
|
|
180
230
|
boundingBox: [minimum, maximum],
|
package/src/v5/mesh-draco.ts
CHANGED
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
validateMeshGeometry
|
|
15
15
|
} from './mesh.js';
|
|
16
16
|
|
|
17
|
+
import {prepareMeshFeatureGeometry, encodeMeshFeatures} from './mesh-features.js';
|
|
18
|
+
|
|
17
19
|
const DRACO_EXTENSION = 'KHR_draco_mesh_compression';
|
|
18
20
|
|
|
19
21
|
/** Encodes the validated single-mesh profile with lossless Edge Breaker and no raw fallback. */
|
|
@@ -22,7 +24,7 @@ export async function encodeDracoMeshTile(
|
|
|
22
24
|
options: MeshTileOptions,
|
|
23
25
|
libraryOptions: LoadLibraryOptions = {}
|
|
24
26
|
): Promise<{glb: ArrayBuffer; positions: Float32Array}> {
|
|
25
|
-
geometry = validateMeshGeometry(geometry);
|
|
27
|
+
geometry = prepareMeshFeatureGeometry(validateMeshGeometry(geometry), options.features);
|
|
26
28
|
// Draco requires explicit faces even when the input triangle list is non-indexed.
|
|
27
29
|
if (!geometry.indices) {
|
|
28
30
|
geometry.indices = {
|
|
@@ -72,6 +74,12 @@ export async function encodeDracoMeshTile(
|
|
|
72
74
|
);
|
|
73
75
|
}
|
|
74
76
|
}
|
|
77
|
+
if (JSON.stringify(getMeshTriangles(geometry)) !== JSON.stringify(getMeshTriangles(decoded))) {
|
|
78
|
+
throw new TileConversionError(
|
|
79
|
+
'MESH_DRACO_ATTRIBUTE_CHANGED',
|
|
80
|
+
'Lossless Draco must preserve oriented triangles and every attribute, including feature ownership'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
75
83
|
const bufferView = scenegraph.addBufferView(encoded.data);
|
|
76
84
|
const attributes: Record<string, number> = {};
|
|
77
85
|
const attributeIdentifiers: Record<string, number> = {};
|
|
@@ -90,9 +98,18 @@ export async function encodeDracoMeshTile(
|
|
|
90
98
|
? 5121
|
|
91
99
|
: attribute.value instanceof Uint16Array
|
|
92
100
|
? 5123
|
|
93
|
-
:
|
|
101
|
+
: attribute.value instanceof Uint32Array
|
|
102
|
+
? 5125
|
|
103
|
+
: 5126,
|
|
94
104
|
count: encoded.report.pointCount,
|
|
95
|
-
type:
|
|
105
|
+
type:
|
|
106
|
+
attribute.size === 1
|
|
107
|
+
? 'SCALAR'
|
|
108
|
+
: attribute.size === 2
|
|
109
|
+
? 'VEC2'
|
|
110
|
+
: attribute.size === 3
|
|
111
|
+
? 'VEC3'
|
|
112
|
+
: 'VEC4',
|
|
96
113
|
...(attribute.normalized ? {normalized: true} : {})
|
|
97
114
|
};
|
|
98
115
|
if (name === 'POSITION') {
|
|
@@ -128,6 +145,7 @@ export async function encodeDracoMeshTile(
|
|
|
128
145
|
]
|
|
129
146
|
}
|
|
130
147
|
];
|
|
148
|
+
encodeMeshFeatures(scenegraph, 0, options.features);
|
|
131
149
|
scenegraph.registerRequiredExtension(DRACO_EXTENSION);
|
|
132
150
|
return {glb: finalizeMeshTileScenegraph(scenegraph, 0), positions};
|
|
133
151
|
}
|
|
@@ -136,3 +154,29 @@ export async function encodeDracoMeshTile(
|
|
|
136
154
|
function getPositionKey(positions: ArrayLike<number>, index: number): string {
|
|
137
155
|
return `${positions[index]},${positions[index + 1]},${positions[index + 2]}`;
|
|
138
156
|
}
|
|
157
|
+
|
|
158
|
+
/** Compares oriented triangles independently of vertex/face order, retaining every attribute. */
|
|
159
|
+
function getMeshTriangles(mesh: Pick<MeshGeometry, 'attributes' | 'indices'>): string[] {
|
|
160
|
+
const names = Object.keys(mesh.attributes).sort();
|
|
161
|
+
const indices = mesh.indices!.value;
|
|
162
|
+
const triangles: string[] = [];
|
|
163
|
+
for (let index = 0; index < indices.length; index += 3) {
|
|
164
|
+
const vertices = [0, 1, 2].map(corner =>
|
|
165
|
+
names
|
|
166
|
+
.map(name => {
|
|
167
|
+
const attribute = mesh.attributes[name];
|
|
168
|
+
return Array.from(
|
|
169
|
+
{length: attribute.size},
|
|
170
|
+
(_, component) => attribute.value[indices[index + corner] * attribute.size + component]
|
|
171
|
+
).join(',');
|
|
172
|
+
})
|
|
173
|
+
.join('|')
|
|
174
|
+
);
|
|
175
|
+
triangles.push(
|
|
176
|
+
[0, 1, 2]
|
|
177
|
+
.map(corner => [...vertices.slice(corner), ...vertices.slice(0, corner)].join(';'))
|
|
178
|
+
.sort()[0]
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return triangles.sort();
|
|
182
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {convertArrowToSchema} from '@loaders.gl/schema-utils';
|
|
6
|
+
import type {MeshGeometry, Field} from '@loaders.gl/schema';
|
|
7
|
+
import type {GLTFScenegraph} from '@loaders.gl/gltf';
|
|
8
|
+
import type {I3SMeshFeatures} from '@loaders.gl/i3s';
|
|
9
|
+
import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
|
|
10
|
+
|
|
11
|
+
/** Explicit-schema Arrow features; stable identifiers are distinct from geometry row indices. */
|
|
12
|
+
export type MeshTileFeatures = Pick<
|
|
13
|
+
I3SMeshFeatures,
|
|
14
|
+
'batches' | 'triangleFeatureIndices' | 'featureIdField'
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
/** Expands shared vertices so each complete triangle retains its own feature row through Draco. */
|
|
18
|
+
export function prepareMeshFeatureGeometry(
|
|
19
|
+
mesh: MeshGeometry,
|
|
20
|
+
features?: MeshTileFeatures
|
|
21
|
+
): MeshGeometry {
|
|
22
|
+
if (!features) return mesh;
|
|
23
|
+
const count = features.batches.reduce((total, batch) => total + batch.data.numRows, 0);
|
|
24
|
+
const indices = mesh.indices?.value;
|
|
25
|
+
const vertexCount = indices?.length ?? mesh.attributes.POSITION.value.length / 3;
|
|
26
|
+
const associations = features.triangleFeatureIndices;
|
|
27
|
+
if (
|
|
28
|
+
!(associations instanceof Uint32Array) ||
|
|
29
|
+
associations.length !== vertexCount / 3 ||
|
|
30
|
+
associations.some(row => row >= count) ||
|
|
31
|
+
new Set(associations).size !== count
|
|
32
|
+
) {
|
|
33
|
+
throw new TileConversionError(
|
|
34
|
+
'MESH_FEATURE_ASSOCIATION_INVALID',
|
|
35
|
+
'Every feature row must own a triangle and every triangle must reference a valid row'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (count > 2 ** 24)
|
|
39
|
+
throw new TileConversionError(
|
|
40
|
+
'MESH_FEATURE_COUNT_UNSUPPORTED',
|
|
41
|
+
'Feature row indices must be exactly representable in a glTF vertex attribute'
|
|
42
|
+
);
|
|
43
|
+
const attributes: MeshGeometry['attributes'] = Object.fromEntries(
|
|
44
|
+
Object.entries(mesh.attributes).map(([name, attribute]) => {
|
|
45
|
+
const values = attribute.value;
|
|
46
|
+
const Constructor = values.constructor as {new (length: number): typeof values};
|
|
47
|
+
const output = new Constructor(vertexCount * attribute.size);
|
|
48
|
+
for (let vertex = 0; vertex < vertexCount; vertex++) {
|
|
49
|
+
const sourceVertex = indices?.[vertex] ?? vertex;
|
|
50
|
+
for (let component = 0; component < attribute.size; component++)
|
|
51
|
+
output[vertex * attribute.size + component] = Number(
|
|
52
|
+
values[sourceVertex * attribute.size + component]
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return [name, {...attribute, value: output}];
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
const featureIndices =
|
|
59
|
+
count <= 65536 ? new Uint16Array(vertexCount) : new Float32Array(vertexCount);
|
|
60
|
+
for (let vertex = 0; vertex < vertexCount; vertex++)
|
|
61
|
+
featureIndices[vertex] = associations[Math.floor(vertex / 3)];
|
|
62
|
+
attributes._FEATURE_ID_0 = {value: featureIndices, size: 1};
|
|
63
|
+
return {...mesh, indices: undefined, attributes};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Writes one inline structural metadata table without number coercion or nullable sentinel collisions. */
|
|
67
|
+
export function encodeMeshFeatures(
|
|
68
|
+
scenegraph: GLTFScenegraph,
|
|
69
|
+
meshIndex: number,
|
|
70
|
+
features?: MeshTileFeatures
|
|
71
|
+
): void {
|
|
72
|
+
if (!features) return;
|
|
73
|
+
const {batches, featureIdField} = features;
|
|
74
|
+
const schema = batches[0]?.schema;
|
|
75
|
+
const count = batches.reduce((total, batch) => total + batch.data.numRows, 0);
|
|
76
|
+
const metadataClass = schema?.metadata?.['loaders.gl:feature-class'] || 'features';
|
|
77
|
+
if (
|
|
78
|
+
!schema ||
|
|
79
|
+
!count ||
|
|
80
|
+
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(metadataClass) ||
|
|
81
|
+
!schema.fields.some(field => field.name === featureIdField && field.nullable !== true) ||
|
|
82
|
+
new Set(schema.fields.map(field => field.name)).size !== schema.fields.length
|
|
83
|
+
) {
|
|
84
|
+
throw new TileConversionError(
|
|
85
|
+
'MESH_FEATURE_SCHEMA_INVALID',
|
|
86
|
+
'Features require a single named class, unique fields, and a non-null stable identifier'
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
for (const batch of batches) {
|
|
90
|
+
const actual = convertArrowToSchema(batch.data.schema).fields;
|
|
91
|
+
if (
|
|
92
|
+
JSON.stringify(batch.schema) !== JSON.stringify(schema) ||
|
|
93
|
+
batch.length !== batch.data.numRows ||
|
|
94
|
+
JSON.stringify(actual.map(field => [field.name, field.type])) !==
|
|
95
|
+
JSON.stringify(schema.fields.map(field => [field.name, field.type]))
|
|
96
|
+
) {
|
|
97
|
+
throw new TileConversionError(
|
|
98
|
+
'MESH_FEATURE_SCHEMA_INVALID',
|
|
99
|
+
'Declared feature schemas and Arrow column types must agree across batches'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const properties: Record<string, object> = Object.create(null);
|
|
104
|
+
const columns: Record<string, object> = Object.create(null);
|
|
105
|
+
for (const field of schema.fields) {
|
|
106
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(field.name))
|
|
107
|
+
throw new TileConversionError(
|
|
108
|
+
'MESH_FEATURE_SCHEMA_INVALID',
|
|
109
|
+
'Property names must be metadata identifiers'
|
|
110
|
+
);
|
|
111
|
+
const values = batches.flatMap(batch => {
|
|
112
|
+
const column = batch.data.getChild(field.name);
|
|
113
|
+
if (!column)
|
|
114
|
+
throw new TileConversionError(
|
|
115
|
+
'MESH_FEATURE_SCHEMA_INVALID',
|
|
116
|
+
`Missing Arrow column ${field.name}`
|
|
117
|
+
);
|
|
118
|
+
return Array.from({length: column.length}, (_, index) => column.get(index));
|
|
119
|
+
});
|
|
120
|
+
if (
|
|
121
|
+
field.name === featureIdField &&
|
|
122
|
+
(values.some(
|
|
123
|
+
value =>
|
|
124
|
+
!(typeof value === 'string' || typeof value === 'bigint' || Number.isSafeInteger(value))
|
|
125
|
+
) ||
|
|
126
|
+
new Set(values).size !== count)
|
|
127
|
+
)
|
|
128
|
+
throw new TileConversionError(
|
|
129
|
+
'MESH_FEATURE_IDS_INVALID',
|
|
130
|
+
'Stable identifiers must be unique strings or exact integers'
|
|
131
|
+
);
|
|
132
|
+
const encoded = encodeFeatureColumn(field, values);
|
|
133
|
+
properties[field.name] = encoded.property;
|
|
134
|
+
columns[field.name] = {
|
|
135
|
+
values: addMetadataBuffer(scenegraph, encoded.data),
|
|
136
|
+
...(encoded.offsets
|
|
137
|
+
? {
|
|
138
|
+
stringOffsets: addMetadataBuffer(scenegraph, encoded.offsets),
|
|
139
|
+
stringOffsetType: 'UINT32'
|
|
140
|
+
}
|
|
141
|
+
: {})
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
scenegraph.json.extensions ||= {};
|
|
145
|
+
scenegraph.json.extensions.EXT_structural_metadata = {
|
|
146
|
+
schema: {id: 'loaders_gl_features', classes: {[metadataClass]: {properties}}},
|
|
147
|
+
propertyTables: [{class: metadataClass, count, properties: columns}]
|
|
148
|
+
};
|
|
149
|
+
const primitive = scenegraph.json.meshes![meshIndex].primitives[0];
|
|
150
|
+
primitive.extensions ||= {};
|
|
151
|
+
primitive.extensions.EXT_mesh_features = {
|
|
152
|
+
featureIds: [{featureCount: count, attribute: 0, propertyTable: 0}]
|
|
153
|
+
};
|
|
154
|
+
scenegraph.registerUsedExtension('EXT_structural_metadata');
|
|
155
|
+
scenegraph.registerUsedExtension('EXT_mesh_features');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Aligns every metadata buffer to eight bytes, including INT64, UINT64 and FLOAT64 columns. */
|
|
159
|
+
function addMetadataBuffer(scenegraph: GLTFScenegraph, data: ArrayBufferView): number {
|
|
160
|
+
if (scenegraph.byteLength % 8) scenegraph.addBufferView(new Uint8Array(4));
|
|
161
|
+
return scenegraph.addBufferView(data);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Encodes supported scalar values and chooses an unused noData value for nullable columns. */
|
|
165
|
+
function encodeFeatureColumn(
|
|
166
|
+
field: Field,
|
|
167
|
+
values: unknown[]
|
|
168
|
+
): {property: object; data: Uint8Array; offsets?: Uint32Array} {
|
|
169
|
+
const type = field.type;
|
|
170
|
+
const hasNull = values.includes(null);
|
|
171
|
+
if (hasNull && field.nullable !== true)
|
|
172
|
+
throw new TileConversionError(
|
|
173
|
+
'MESH_FEATURE_VALUE_INVALID',
|
|
174
|
+
`Required field ${field.name} contains null`
|
|
175
|
+
);
|
|
176
|
+
const present = values.filter(value => value !== null);
|
|
177
|
+
let noData: string | number | undefined;
|
|
178
|
+
if (type === 'utf8') {
|
|
179
|
+
if (
|
|
180
|
+
present.some(
|
|
181
|
+
value =>
|
|
182
|
+
typeof value !== 'string' ||
|
|
183
|
+
new TextDecoder().decode(new TextEncoder().encode(value)) !== value
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
throw new TileConversionError('MESH_FEATURE_VALUE_INVALID', 'Strings require valid Unicode');
|
|
187
|
+
if (hasNull) {
|
|
188
|
+
noData = '';
|
|
189
|
+
const strings = new Set(present);
|
|
190
|
+
while (strings.has(noData)) noData += '_';
|
|
191
|
+
}
|
|
192
|
+
const encoded = values.map(value =>
|
|
193
|
+
new TextEncoder().encode(value === null ? (noData as string) : (value as string))
|
|
194
|
+
);
|
|
195
|
+
const offsets = new Uint32Array(values.length + 1);
|
|
196
|
+
for (let index = 0; index < values.length; index++)
|
|
197
|
+
offsets[index + 1] = offsets[index] + encoded[index].length;
|
|
198
|
+
const data = new Uint8Array(offsets[values.length]);
|
|
199
|
+
encoded.forEach((bytes, index) => data.set(bytes, offsets[index]));
|
|
200
|
+
return {
|
|
201
|
+
property: {type: 'STRING', required: field.nullable !== true, ...(hasNull ? {noData} : {})},
|
|
202
|
+
data,
|
|
203
|
+
offsets
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const widths: Record<string, number> = {
|
|
207
|
+
int8: 1,
|
|
208
|
+
uint8: 1,
|
|
209
|
+
int16: 2,
|
|
210
|
+
uint16: 2,
|
|
211
|
+
int32: 4,
|
|
212
|
+
uint32: 4,
|
|
213
|
+
int64: 8,
|
|
214
|
+
uint64: 8,
|
|
215
|
+
float32: 4,
|
|
216
|
+
float64: 8
|
|
217
|
+
};
|
|
218
|
+
const width = widths[String(type)];
|
|
219
|
+
if (!width)
|
|
220
|
+
throw new TileConversionError(
|
|
221
|
+
'MESH_FEATURE_TYPE_UNSUPPORTED',
|
|
222
|
+
`No qualified GLB property mapping for ${String(type)}`
|
|
223
|
+
);
|
|
224
|
+
const isInteger64 = type === 'int64' || type === 'uint64';
|
|
225
|
+
const isFloat = type === 'float32' || type === 'float64';
|
|
226
|
+
const minimum = isFloat
|
|
227
|
+
? -Infinity
|
|
228
|
+
: String(type).startsWith('uint')
|
|
229
|
+
? 0n
|
|
230
|
+
: -(1n << BigInt(width * 8 - 1));
|
|
231
|
+
const maximum = isFloat
|
|
232
|
+
? Infinity
|
|
233
|
+
: (1n << BigInt(width * 8 - (String(type).startsWith('uint') ? 0 : 1))) - 1n;
|
|
234
|
+
if (
|
|
235
|
+
present.some(value =>
|
|
236
|
+
isInteger64
|
|
237
|
+
? typeof value !== 'bigint' || value < minimum || value > maximum
|
|
238
|
+
: typeof value !== 'number' ||
|
|
239
|
+
!Number.isFinite(value) ||
|
|
240
|
+
(!isFloat &&
|
|
241
|
+
(!Number.isSafeInteger(value) || BigInt(value) < minimum || BigInt(value) > maximum))
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
throw new TileConversionError(
|
|
245
|
+
'MESH_FEATURE_VALUE_INVALID',
|
|
246
|
+
`Values do not fit ${String(type)}`
|
|
247
|
+
);
|
|
248
|
+
if (hasNull) {
|
|
249
|
+
const used = new Set(present.map(String));
|
|
250
|
+
noData = 0;
|
|
251
|
+
while (used.has(String(noData))) noData++;
|
|
252
|
+
if (noData > maximum && minimum < 0) {
|
|
253
|
+
noData = -1;
|
|
254
|
+
while (used.has(String(noData))) noData--;
|
|
255
|
+
}
|
|
256
|
+
if (noData > maximum || noData < minimum)
|
|
257
|
+
throw new TileConversionError(
|
|
258
|
+
'MESH_FEATURE_NULL_UNREPRESENTABLE',
|
|
259
|
+
'No unused value remains for null without losing a real value'
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
const data = new Uint8Array(values.length * width);
|
|
263
|
+
const view = new DataView(data.buffer);
|
|
264
|
+
values.forEach((value, index) => {
|
|
265
|
+
const selected = value === null ? (isInteger64 ? BigInt(noData!) : noData) : value;
|
|
266
|
+
const offset = index * width;
|
|
267
|
+
switch (type) {
|
|
268
|
+
case 'int8':
|
|
269
|
+
view.setInt8(offset, selected as number);
|
|
270
|
+
break;
|
|
271
|
+
case 'uint8':
|
|
272
|
+
view.setUint8(offset, selected as number);
|
|
273
|
+
break;
|
|
274
|
+
case 'int16':
|
|
275
|
+
view.setInt16(offset, selected as number, true);
|
|
276
|
+
break;
|
|
277
|
+
case 'uint16':
|
|
278
|
+
view.setUint16(offset, selected as number, true);
|
|
279
|
+
break;
|
|
280
|
+
case 'int32':
|
|
281
|
+
view.setInt32(offset, selected as number, true);
|
|
282
|
+
break;
|
|
283
|
+
case 'uint32':
|
|
284
|
+
view.setUint32(offset, selected as number, true);
|
|
285
|
+
break;
|
|
286
|
+
case 'int64':
|
|
287
|
+
view.setBigInt64(offset, selected as bigint, true);
|
|
288
|
+
break;
|
|
289
|
+
case 'uint64':
|
|
290
|
+
view.setBigUint64(offset, selected as bigint, true);
|
|
291
|
+
break;
|
|
292
|
+
case 'float32':
|
|
293
|
+
view.setFloat32(offset, selected as number, true);
|
|
294
|
+
break;
|
|
295
|
+
case 'float64':
|
|
296
|
+
view.setFloat64(offset, selected as number, true);
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
if (
|
|
300
|
+
isFloat &&
|
|
301
|
+
!Number.isFinite(
|
|
302
|
+
type === 'float32' ? view.getFloat32(offset, true) : view.getFloat64(offset, true)
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
throw new TileConversionError(
|
|
306
|
+
'MESH_FEATURE_VALUE_INVALID',
|
|
307
|
+
'Floating point property overflow'
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
return {
|
|
311
|
+
property: {
|
|
312
|
+
type: 'SCALAR',
|
|
313
|
+
componentType: String(type).toUpperCase(),
|
|
314
|
+
required: field.nullable !== true,
|
|
315
|
+
...(hasNull ? {noData} : {})
|
|
316
|
+
},
|
|
317
|
+
data
|
|
318
|
+
};
|
|
319
|
+
}
|