usd-gltf 0.3.0 → 0.3.1

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 CHANGED
@@ -52,6 +52,21 @@ import draco3d from 'draco3d';
52
52
  const usdz = await glbToUSD( glb, { dracoDecoder: await draco3d.createDecoderModule() } );
53
53
  ```
54
54
 
55
+ The extension requires accessors to describe the decoded data exactly. When they don't, conversion rejects with a `DracoMismatchError`. Its `kind` says what disagrees: `'count'`, `'index-count'`, `'components'`, `'component-type'`, `'attribute'` or `'shared-accessor'`. Its `mesh`, `primitive`, `semantic`, `accessor`, `declared` and `decoded` fields say where and by how much.
56
+
57
+ Some files declare stale counts, for example from metadata written before compression changed the vertex layout. `recoverDracoCounts: true` converts those using the decoded counts and reports each one through `log`. It only does so when everything else that depends on those counts can be checked against the decoded data:
58
+
59
+ - the accessors are used only by Draco primitives decoding the same data;
60
+ - the primitive's uncompressed attributes and morph targets have the decoded vertex count;
61
+ - the accessors aren't sparse;
62
+ - the decoded positions match `POSITION`'s `min` and `max`.
63
+
64
+ Otherwise it still rejects, with `unrecoverable` saying why. Other mismatches, such as a different number of components, are never recovered. The decoder's output is sized by the decoded data, never by the declared counts, and counts against the limits below either way.
65
+
66
+ ```js
67
+ const usdz = await glbToUSD( glb, { recoverDracoCounts: true, log: console.warn } );
68
+ ```
69
+
55
70
  ## Limits
56
71
 
57
72
  Input size alone doesn't bound how much memory a conversion needs: an accessor can declare millions of vertices with no data behind them, and compressed geometry and images expand when decoded. Both directions take opt-in limits:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usd-gltf",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Convert between USD (usda, usdc, usdz) and glTF / GLB, pure JavaScript with no dependencies",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { readGLB, writeGLB } from './glb.js';
4
4
  import { decodeMeshoptView } from './meshopt.js';
5
- import { decodeDracoPrimitive, loadDracoDecoder, needsDraco } from './draco.js';
5
+ import { decodeDracoPrimitive, loadDracoDecoder, needsDraco, resolveDracoAccessors } from './draco.js';
6
6
  import { Budget, checkSize } from '../limits.js';
7
7
 
8
8
  const COMPONENT_ARRAYS = {
@@ -215,9 +215,11 @@ export class GLTFDocument {
215
215
  /**
216
216
  * Loads a glTF or GLB.
217
217
  * @param {Uint8Array} bytes
218
- * @param {{ resolver?: ( uri: string ) => Promise<Uint8Array>|Uint8Array, dracoDecoder?: object|Promise<object> }} [options] resolver loads external uris (already decoded from URI escaping); dracoDecoder is a Draco decoder module, loaded when needed if not given
218
+ * @param {{ resolver?: ( uri: string ) => Promise<Uint8Array>|Uint8Array, dracoDecoder?: object|Promise<object>, recoverDracoCounts?: boolean, log?: ( message: string ) => void }} [options]
219
+ * resolver loads external uris (already decoded from URI escaping); dracoDecoder is a Draco decoder module, loaded when needed if not given;
220
+ * recoverDracoCounts uses decoded Draco counts that disagree with their accessors when that can be checked, and logs it
219
221
  */
220
- export async function readGLTF( bytes, { resolver = null, budget = new Budget(), dracoDecoder = null } = {} ) {
222
+ export async function readGLTF( bytes, { resolver = null, budget = new Budget(), dracoDecoder = null, recoverDracoCounts = false, log = () => {} } = {} ) {
221
223
 
222
224
  let json, bin = null;
223
225
  bytes = plainBytes( bytes );
@@ -268,19 +270,19 @@ export async function readGLTF( bytes, { resolver = null, budget = new Budget(),
268
270
  } );
269
271
 
270
272
  // Draco primitives without uncompressed fallback data
271
- const draco = ( json.meshes || [] ).flatMap( ( mesh ) => mesh.primitives ).filter( ( p ) => needsDraco( json, p ) );
273
+ const draco = ( json.meshes || [] ).flatMap( ( mesh, m ) => ( mesh.primitives || [] ).map( ( primitive, index ) => ( { mesh: m, index, primitive } ) ) ).filter( ( p ) => needsDraco( json, p.primitive ) );
272
274
 
273
275
  if ( draco.length ) {
274
276
 
275
277
  const decoder = await ( dracoDecoder || loadDracoDecoder() );
276
-
277
- for ( const primitive of draco ) {
278
+ const decoded = draco.map( ( where ) => {
278
279
 
279
280
  budget.check();
280
- const bytes = doc.bufferViewBytes( primitive.extensions.KHR_draco_mesh_compression.bufferView );
281
- for ( const [ index, data ] of decodeDracoPrimitive( decoder, bytes, json, primitive, budget ) ) doc.decodedAccessors.set( index, data );
281
+ const bytes = doc.bufferViewBytes( where.primitive.extensions.KHR_draco_mesh_compression.bufferView );
282
+ return { ...where, ...decodeDracoPrimitive( decoder, bytes, json, where, budget ) };
282
283
 
283
- }
284
+ } );
285
+ doc.decodedAccessors = resolveDracoAccessors( json, decoded, { recoverCounts: recoverDracoCounts, log } );
284
286
 
285
287
  }
286
288
 
package/src/gltf/draco.js CHANGED
@@ -12,6 +12,8 @@ const CDN_FILES = {
12
12
 
13
13
  const DATA_TYPES = { 5120: 'DT_INT8', 5121: 'DT_UINT8', 5122: 'DT_INT16', 5123: 'DT_UINT16', 5125: 'DT_UINT32', 5126: 'DT_FLOAT32' };
14
14
  const COMPONENT_ARRAYS = { 5120: Int8Array, 5121: Uint8Array, 5122: Int16Array, 5123: Uint16Array, 5125: Uint32Array, 5126: Float32Array };
15
+ const INDEX_ARRAYS = { 5121: Uint8Array, 5123: Uint16Array, 5125: Uint32Array };
16
+ const INDEX_MAX = { 5121: 0xFF, 5123: 0xFFFF, 5125: 0xFFFFFFFF };
15
17
  const TYPE_SIZES = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 };
16
18
 
17
19
  let defaultDecoder = null;
@@ -93,21 +95,65 @@ export function needsDraco( json, primitive ) {
93
95
  }
94
96
 
95
97
  /**
96
- * Decodes one Draco compressed primitive into the data of the accessors it describes.
98
+ * Thrown when decoded Draco data doesn't match the accessors describing it, which the extension
99
+ * requires. kind says what disagrees:
100
+ * - 'count': an attribute decodes to a different number of elements than its accessor's count
101
+ * - 'index-count': the decoded triangles need a different number of indices than declared
102
+ * - 'components': an attribute decodes to a different number of components than its type has
103
+ * - 'component-type': the accessor's componentType can't hold the decoded values
104
+ * - 'attribute': the Draco data has no attribute with the extension's id
105
+ * - 'shared-accessor': one accessor is decoded more than once, to different data
106
+ * With recoverDracoCounts, count mismatches that can be checked are recovered instead; when one
107
+ * can't be, unrecoverable says why.
108
+ */
109
+ export class DracoMismatchError extends Error {
110
+
111
+ constructor( { kind, mesh, primitive, semantic, accessor, declared, decoded, unrecoverable = null } ) {
112
+
113
+ const what = {
114
+ 'count': () => `${ semantic } decodes to ${ decoded } elements, but accessor ${ accessor } declares ${ declared }`,
115
+ 'index-count': () => `the triangles decode to ${ decoded } indices, but accessor ${ accessor } declares ${ declared }`,
116
+ 'components': () => `${ semantic } decodes to ${ decoded } components, but accessor ${ accessor } is ${ declared }`,
117
+ 'component-type': () => `${ semantic } decodes to values accessor ${ accessor } can't hold as componentType ${ declared }`,
118
+ 'attribute': () => `there is no ${ semantic } attribute with id ${ declared }`,
119
+ 'shared-accessor': () => `accessor ${ accessor } (${ semantic }) is decoded more than once, to different data`,
120
+ }[ kind ]();
121
+ const counts = kind === 'count' || kind === 'index-count';
122
+ const hint = unrecoverable ? `; not recovered, as ${ unrecoverable }` : counts ? '; the recoverDracoCounts option can use the decoded count' : '';
123
+ super( `usd-gltf: Draco mesh ${ mesh } primitive ${ primitive }: ${ what }${ hint }` );
124
+ this.name = 'DracoMismatchError';
125
+ this.code = 'USD_GLTF_DRACO_MISMATCH';
126
+ Object.assign( this, { kind, mesh, primitive, semantic, accessor, declared, decoded, unrecoverable } );
127
+
128
+ }
129
+
130
+ }
131
+
132
+ /**
133
+ * Decodes one Draco compressed primitive. Allocations are sized by the decoded data, never by
134
+ * the declared accessor counts, which resolveDracoAccessors checks afterwards.
97
135
  * @param {object} draco decoder module
98
136
  * @param {Uint8Array} bytes the extension's bufferView
99
137
  * @param {object} json
100
- * @param {object} primitive
138
+ * @param {{ mesh: number, index: number, primitive: object }} where the primitive and its place in the document
101
139
  * @param {import('../limits.js').Budget} budget
102
- * @returns {Map<number, ArrayLike<number>>} accessor index -> data, in the accessor's component type
140
+ * @returns {{ points: number, indexCount: number|null, data: Map<number, ArrayLike<number>> }} data is accessor index -> values, in the accessor's component type
103
141
  */
104
- export function decodeDracoPrimitive( draco, bytes, json, primitive, budget ) {
142
+ export function decodeDracoPrimitive( draco, bytes, json, { mesh, index: primitiveIndex, primitive }, budget ) {
105
143
 
106
144
  const ext = primitive.extensions.KHR_draco_mesh_compression;
107
145
  const decoder = new draco.Decoder();
108
146
  const array = new Int8Array( bytes.buffer, bytes.byteOffset, bytes.byteLength );
109
- const out = new Map();
110
- let geometry = null;
147
+ const mismatch = ( info ) => new DracoMismatchError( { mesh, primitive: primitiveIndex, ...info } );
148
+ const data = new Map();
149
+ let geometry = null, points = 0, indexCount = null;
150
+
151
+ const put = ( semantic, accessor, values ) => {
152
+
153
+ if ( data.has( accessor ) && ! sameValues( data.get( accessor ), values ) ) throw mismatch( { kind: 'shared-accessor', semantic, accessor } );
154
+ data.set( accessor, values );
155
+
156
+ };
111
157
 
112
158
  try {
113
159
 
@@ -132,30 +178,36 @@ export function decodeDracoPrimitive( draco, bytes, json, primitive, budget ) {
132
178
 
133
179
  if ( ! status.ok() || geometry.ptr === 0 ) throw new Error( `usd-gltf: Draco decoding failed: ${ status.error_msg() }` );
134
180
 
135
- const points = geometry.num_points();
181
+ points = geometry.num_points();
136
182
 
137
- for ( const [ name, id ] of Object.entries( ext.attributes ) ) {
183
+ for ( const [ semantic, id ] of Object.entries( ext.attributes ) ) {
138
184
 
139
- const index = primitive.attributes[ name ];
185
+ const index = primitive.attributes[ semantic ];
140
186
  const accessor = json.accessors[ index ];
141
187
  if ( accessor === undefined ) continue;
142
188
  const ArrayType = COMPONENT_ARRAYS[ accessor.componentType ];
143
189
  const size = TYPE_SIZES[ accessor.type ];
190
+ if ( ! size ) throw new Error( `usd-gltf: accessor ${ index } has an invalid type` );
191
+ if ( ! ArrayType ) throw mismatch( { kind: 'component-type', semantic, accessor: index, declared: accessor.componentType } );
144
192
  const attribute = decoder.GetAttributeByUniqueId( geometry, id );
145
- if ( ! ArrayType || ! size || ! attribute || attribute.ptr === 0 ) throw new Error( `usd-gltf: Draco data has no ${ name } attribute ${ id }` );
146
- if ( points !== accessor.count || attribute.num_components() !== size ) throw new Error( `usd-gltf: Draco ${ name } doesn't match accessor ${ index }` );
147
- out.set( index, read( draco, points * size, ArrayType, budget, ( length, ptr ) => decoder.GetAttributeDataArrayForAllPoints( geometry, attribute, draco[ DATA_TYPES[ accessor.componentType ] ], length, ptr ) ) );
193
+ if ( ! attribute || attribute.ptr === 0 ) throw mismatch( { kind: 'attribute', semantic, accessor: index, declared: id } );
194
+ if ( attribute.num_components() !== size ) throw mismatch( { kind: 'components', semantic, accessor: index, declared: accessor.type, decoded: attribute.num_components() } );
195
+ put( semantic, index, read( draco, points * size, ArrayType, budget, ( length, ptr ) => decoder.GetAttributeDataArrayForAllPoints( geometry, attribute, draco[ DATA_TYPES[ accessor.componentType ] ], length, ptr ) ) );
148
196
 
149
197
  }
150
198
 
151
199
  if ( primitive.indices !== undefined && type === draco.TRIANGULAR_MESH ) {
152
200
 
153
201
  const accessor = json.accessors[ primitive.indices ];
154
- const count = geometry.num_faces() * 3;
155
- if ( ! accessor || accessor.count !== count ) throw new Error( `usd-gltf: Draco indices don't match accessor ${ primitive.indices }` );
156
- const indices = read( draco, count, Uint32Array, budget, ( length, ptr ) => decoder.GetTrianglesUInt32Array( geometry, length, ptr ) );
157
- const IndexType = COMPONENT_ARRAYS[ accessor.componentType ] || Uint32Array;
158
- out.set( primitive.indices, IndexType === Uint32Array ? indices : IndexType.from( indices ) );
202
+ if ( ! accessor ) throw new Error( `usd-gltf: accessor ${ primitive.indices } doesn't exist` );
203
+ indexCount = geometry.num_faces() * 3;
204
+ const indices = read( draco, indexCount, Uint32Array, budget, ( length, ptr ) => decoder.GetTrianglesUInt32Array( geometry, length, ptr ) );
205
+ let max = - 1;
206
+ for ( let i = 0; i < indices.length; i ++ ) if ( indices[ i ] > max ) max = indices[ i ];
207
+ if ( max >= points ) throw new Error( 'usd-gltf: Draco indices are out of range' );
208
+ const IndexType = INDEX_ARRAYS[ accessor.componentType ];
209
+ if ( ! IndexType || max > INDEX_MAX[ accessor.componentType ] ) throw mismatch( { kind: 'component-type', semantic: 'indices', accessor: primitive.indices, declared: accessor.componentType } );
210
+ put( 'indices', primitive.indices, IndexType === Uint32Array ? indices : IndexType.from( indices ) );
159
211
 
160
212
  }
161
213
 
@@ -166,10 +218,172 @@ export function decodeDracoPrimitive( draco, bytes, json, primitive, budget ) {
166
218
 
167
219
  }
168
220
 
221
+ return { points, indexCount, data };
222
+
223
+ }
224
+
225
+ /**
226
+ * Checks decoded primitives against their accessors and returns the accessor data. Count
227
+ * mismatches are errors, unless recoverCounts is set and everything else that depends on those
228
+ * counts agrees with the decoded data: then the accessors take the decoded counts, and log says so.
229
+ * @param {object} json changed only by recovered counts
230
+ * @param {Array<{ mesh: number, index: number, primitive: object, points: number, indexCount: number|null, data: Map<number, ArrayLike<number>> }>} decoded
231
+ * @param {{ recoverCounts?: boolean, log?: ( message: string ) => void }} [options]
232
+ * @returns {Map<number, ArrayLike<number>>}
233
+ */
234
+ export function resolveDracoAccessors( json, decoded, { recoverCounts = false, log = () => {} } = {} ) {
235
+
236
+ const out = new Map();
237
+
238
+ for ( const d of decoded ) for ( const [ index, values ] of d.data ) {
239
+
240
+ if ( out.has( index ) && ! sameValues( out.get( index ), values ) ) throw new DracoMismatchError( { kind: 'shared-accessor', mesh: d.mesh, primitive: d.index, semantic: semanticOf( d.primitive, index ), accessor: index } );
241
+ out.set( index, values );
242
+
243
+ }
244
+
245
+ const uses = recoverCounts ? accessorUses( json ) : null;
246
+ const recovered = new Map(); // accessor -> decoded count
247
+
248
+ for ( const d of decoded ) {
249
+
250
+ const { primitive } = d;
251
+ const mismatches = [];
252
+
253
+ for ( const semantic of Object.keys( primitive.extensions.KHR_draco_mesh_compression.attributes ) ) {
254
+
255
+ const index = primitive.attributes[ semantic ];
256
+ const declared = json.accessors[ index ]?.count;
257
+ if ( index !== undefined && declared !== d.points ) mismatches.push( { kind: 'count', semantic, accessor: index, declared, decoded: d.points } );
258
+
259
+ }
260
+
261
+ const declaredIndices = d.indexCount !== null ? json.accessors[ primitive.indices ].count : null;
262
+ if ( declaredIndices !== d.indexCount ) mismatches.push( { kind: 'index-count', semantic: 'indices', accessor: primitive.indices, declared: declaredIndices, decoded: d.indexCount } );
263
+
264
+ if ( ! mismatches.length ) continue;
265
+ const fail = ( unrecoverable ) => new DracoMismatchError( { mesh: d.mesh, primitive: d.index, ...mismatches[ 0 ], unrecoverable } );
266
+ if ( ! recoverCounts ) throw fail( null );
267
+ const reason = unrecoverable( json, d, mismatches, uses, out );
268
+ if ( reason ) throw fail( reason );
269
+
270
+ for ( const m of mismatches ) recovered.set( m.accessor, m.decoded );
271
+ log( `Draco mesh ${ d.mesh } primitive ${ d.index }: using the decoded counts, ${ mismatches.map( ( m ) => `${ m.semantic } ${ m.decoded } where accessor ${ m.accessor } declares ${ m.declared }` ).join( ', ' ) }` );
272
+
273
+ }
274
+
275
+ for ( const [ index, count ] of recovered ) json.accessors[ index ].count = count;
169
276
  return out;
170
277
 
171
278
  }
172
279
 
280
+ // Why a primitive's mismatched counts can't be replaced by the decoded ones, or null when they can.
281
+ function unrecoverable( json, d, mismatches, uses, decodedData ) {
282
+
283
+ const { primitive, points } = d;
284
+ const compressed = primitive.extensions.KHR_draco_mesh_compression.attributes;
285
+
286
+ for ( const m of mismatches ) {
287
+
288
+ if ( json.accessors[ m.accessor ].sparse ) return `accessor ${ m.accessor } is sparse`;
289
+ // other Draco primitives decoding the same data are checked on their own
290
+ const other = uses.get( m.accessor ).find( ( use ) => ! use.draco );
291
+ if ( other ) return `accessor ${ m.accessor } is also used by ${ other.what }`;
292
+
293
+ }
294
+
295
+ for ( const [ semantic, index ] of Object.entries( primitive.attributes ) ) {
296
+
297
+ const count = json.accessors[ index ]?.count;
298
+ if ( ! ( semantic in compressed ) && count !== points ) return `${ semantic } isn't Draco compressed and has ${ count } elements, not ${ points }`;
299
+
300
+ }
301
+
302
+ for ( const [ t, target ] of ( primitive.targets || [] ).entries() ) {
303
+
304
+ for ( const [ semantic, index ] of Object.entries( target ) ) {
305
+
306
+ const count = json.accessors[ index ]?.count;
307
+ if ( count !== points ) return `morph target ${ t } ${ semantic } has ${ count } elements, not ${ points }`;
308
+
309
+ }
310
+
311
+ }
312
+
313
+ const position = primitive.attributes.POSITION;
314
+
315
+ if ( mismatches.some( ( m ) => m.accessor === position ) ) {
316
+
317
+ // the bounds must still describe the decoded positions, allowing for 8 bits of Draco quantization over the largest extent
318
+ const { min, max } = json.accessors[ position ];
319
+ if ( ! ( min?.length === 3 && max?.length === 3 ) ) return `POSITION accessor ${ position } has no min and max to check the decoded positions against`;
320
+ const values = decodedData.get( position );
321
+ const lo = [ Infinity, Infinity, Infinity ], hi = [ - Infinity, - Infinity, - Infinity ];
322
+
323
+ for ( let i = 0; i < values.length; i += 3 ) for ( let c = 0; c < 3; c ++ ) {
324
+
325
+ lo[ c ] = Math.min( lo[ c ], values[ i + c ] );
326
+ hi[ c ] = Math.max( hi[ c ], values[ i + c ] );
327
+
328
+ }
329
+
330
+ const tolerance = Math.max( max[ 0 ] - min[ 0 ], max[ 1 ] - min[ 1 ], max[ 2 ] - min[ 2 ] ) / 256 + 1e-6;
331
+ for ( let c = 0; c < 3; c ++ ) if ( ! ( Math.abs( lo[ c ] - min[ c ] ) <= tolerance && Math.abs( hi[ c ] - max[ c ] ) <= tolerance ) ) return `the decoded positions don't match POSITION accessor ${ position }'s min and max`;
332
+
333
+ }
334
+
335
+ return null;
336
+
337
+ }
338
+
339
+ // Every reference to each accessor, marking those a Draco extension provides.
340
+ function accessorUses( json ) {
341
+
342
+ const uses = new Map();
343
+ const add = ( accessor, what, draco ) => { if ( accessor !== undefined ) uses.set( accessor, [ ...( uses.get( accessor ) || [] ), { what, draco } ] ); };
344
+
345
+ ( json.meshes || [] ).forEach( ( mesh, m ) => ( mesh.primitives || [] ).forEach( ( primitive, p ) => {
346
+
347
+ const draco = primitive.extensions?.KHR_draco_mesh_compression;
348
+ const where = `mesh ${ m } primitive ${ p }`;
349
+ for ( const [ semantic, a ] of Object.entries( primitive.attributes || {} ) ) add( a, `${ where } ${ semantic }`, !! draco && semantic in draco.attributes );
350
+ add( primitive.indices, `${ where } indices`, !! draco );
351
+ ( primitive.targets || [] ).forEach( ( target, t ) => { for ( const [ semantic, a ] of Object.entries( target ) ) add( a, `${ where } morph target ${ t } ${ semantic }`, false ); } );
352
+
353
+ } ) );
354
+
355
+ ( json.skins || [] ).forEach( ( skin, s ) => add( skin.inverseBindMatrices, `skin ${ s }`, false ) );
356
+ ( json.animations || [] ).forEach( ( animation, a ) => ( animation.samplers || [] ).forEach( ( sampler, s ) => {
357
+
358
+ add( sampler.input, `animation ${ a } sampler ${ s }`, false );
359
+ add( sampler.output, `animation ${ a } sampler ${ s }`, false );
360
+
361
+ } ) );
362
+ ( json.nodes || [] ).forEach( ( node, n ) => {
363
+
364
+ for ( const a of Object.values( node.extensions?.EXT_mesh_gpu_instancing?.attributes || {} ) ) add( a, `node ${ n } instancing`, false );
365
+
366
+ } );
367
+
368
+ return uses;
369
+
370
+ }
371
+
372
+ function semanticOf( primitive, accessor ) {
373
+
374
+ if ( primitive.indices === accessor ) return 'indices';
375
+ return Object.keys( primitive.attributes ).find( ( s ) => primitive.attributes[ s ] === accessor );
376
+
377
+ }
378
+
379
+ function sameValues( a, b ) {
380
+
381
+ if ( a.length !== b.length || a.constructor !== b.constructor ) return false;
382
+ for ( let i = 0; i < a.length; i ++ ) if ( ! Object.is( a[ i ], b[ i ] ) ) return false;
383
+ return true;
384
+
385
+ }
386
+
173
387
  // Has the decoder fill a buffer on its heap, then copies it out.
174
388
  function read( draco, count, ArrayType, budget, fill ) {
175
389
 
package/src/index.d.ts CHANGED
@@ -34,6 +34,34 @@ export class ConversionLimitError extends Error {
34
34
  attempted: number;
35
35
  }
36
36
 
37
+ /**
38
+ * Thrown when decoded Draco data doesn't match the accessors describing it. With recoverDracoCounts,
39
+ * count mismatches that can't be checked still throw, with unrecoverable saying why.
40
+ */
41
+ export class DracoMismatchError extends Error {
42
+ name: 'DracoMismatchError';
43
+ code: 'USD_GLTF_DRACO_MISMATCH';
44
+ /**
45
+ * count / index-count: a different number of elements or indices than declared;
46
+ * components: a different number of components than the accessor type has;
47
+ * component-type: values the accessor's componentType can't hold;
48
+ * attribute: no Draco attribute with the extension's id;
49
+ * shared-accessor: one accessor decoded more than once to different data.
50
+ */
51
+ kind: 'count' | 'index-count' | 'components' | 'component-type' | 'attribute' | 'shared-accessor';
52
+ mesh: number;
53
+ primitive: number;
54
+ /** The attribute semantic, e.g. 'POSITION', or 'indices'. */
55
+ semantic: string;
56
+ accessor: number;
57
+ /** What the accessor declares: its count, type, componentType, or the Draco attribute id. */
58
+ declared?: number | string;
59
+ /** What the Draco data has: a count of elements, indices or components. */
60
+ decoded?: number;
61
+ /** With recoverDracoCounts, why the decoded counts couldn't be used. */
62
+ unrecoverable: string | null;
63
+ }
64
+
37
65
  export interface USDToGLBOptions extends ConversionLimits {
38
66
  /** Name of the input file, used to resolve relative paths. */
39
67
  identifier?: string;
@@ -58,7 +86,14 @@ export interface GLBToUSDOptions extends ConversionLimits {
58
86
  * promise of one. Without it, Draco files load draco3d in Node and Google's CDN build in browsers.
59
87
  */
60
88
  dracoDecoder?: object | Promise<object>;
89
+ /**
90
+ * Use decoded Draco counts that disagree with their accessors' counts, which the extension
91
+ * doesn't allow, when everything else depending on them can be checked against the decoded data.
92
+ * Each recovery is reported through log. Default false: such files throw a DracoMismatchError.
93
+ */
94
+ recoverDracoCounts?: boolean;
61
95
  imageDecoder?: ImageDecoder;
96
+ /** Receives non fatal problems, such as textures that could not be read or recovered Draco counts. */
62
97
  log?: ( message: string ) => void;
63
98
  }
64
99
 
package/src/index.js CHANGED
@@ -15,6 +15,7 @@ export { writeUSDA } from './usd/usda/writer.js';
15
15
  export { readUSDC } from './usd/usdc/reader.js';
16
16
  export { writeUSDC } from './usd/usdc/writer.js';
17
17
  export { ConversionLimitError } from './limits.js';
18
+ export { DracoMismatchError } from './gltf/draco.js';
18
19
 
19
20
  function toBytes( data ) {
20
21
 
@@ -50,13 +51,13 @@ export async function usdToGLB( data, options = {} ) {
50
51
  /**
51
52
  * Converts GLB (or glTF JSON with embedded or resolvable buffers) to USD.
52
53
  * @param {ArrayBuffer|Uint8Array} data
53
- * @param {{ format?: 'usda'|'usdc'|'usdz', resolver?: ( uri: string ) => Promise<Uint8Array>, dracoDecoder?: object, maxDecodedBytes?: number, maxOutputBytes?: number, signal?: AbortSignal }} [options]
54
+ * @param {{ format?: 'usda'|'usdc'|'usdz', resolver?: ( uri: string ) => Promise<Uint8Array>, dracoDecoder?: object, recoverDracoCounts?: boolean, log?: ( message: string ) => void, maxDecodedBytes?: number, maxOutputBytes?: number, signal?: AbortSignal }} [options]
54
55
  * @returns {Promise<Uint8Array>}
55
56
  */
56
- export async function glbToUSD( data, { format = 'usdz', resolver = null, log, imageDecoder, dracoDecoder, ...limits } = {} ) {
57
+ export async function glbToUSD( data, { format = 'usdz', resolver = null, log, imageDecoder, dracoDecoder, recoverDracoCounts, ...limits } = {} ) {
57
58
 
58
59
  const budget = budgetFor( limits );
59
- const doc = await readGLTF( toBytes( data ), { resolver, budget, dracoDecoder } );
60
+ const doc = await readGLTF( toBytes( data ), { resolver, budget, dracoDecoder, recoverDracoCounts, log } );
60
61
  const { layer, files } = await new GLTFToUSD( doc, { log, imageDecoder } ).convert();
61
62
  let out;
62
63
 
@@ -73,10 +74,10 @@ export async function glbToUSD( data, { format = 'usdz', resolver = null, log, i
73
74
  * For usda / usdc output the textures are separate files; this returns them alongside the layer.
74
75
  * @returns {Promise<{ layer: Uint8Array, files: Array<{ name: string, data: Uint8Array }> }>}
75
76
  */
76
- export async function glbToUSDFiles( data, { format = 'usda', resolver = null, log, imageDecoder, dracoDecoder, ...limits } = {} ) {
77
+ export async function glbToUSDFiles( data, { format = 'usda', resolver = null, log, imageDecoder, dracoDecoder, recoverDracoCounts, ...limits } = {} ) {
77
78
 
78
79
  const budget = budgetFor( limits );
79
- const doc = await readGLTF( toBytes( data ), { resolver, budget, dracoDecoder } );
80
+ const doc = await readGLTF( toBytes( data ), { resolver, budget, dracoDecoder, recoverDracoCounts, log } );
80
81
  const { layer, files } = await new GLTFToUSD( doc, { log, imageDecoder } ).convert();
81
82
  const bytes = format === 'usdc' ? writeUSDC( layer, { budget } ) : new TextEncoder().encode( writeUSDA( layer, { budget } ) );
82
83
  budget.output( files.reduce( ( n, f ) => n + f.data.byteLength, bytes.byteLength ), 'usd files' );