usd-gltf 0.0.1 → 0.2.0
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 +29 -6
- package/package.json +10 -4
- package/src/convert/gltf-to-usd.js +166 -46
- package/src/convert/usd-material.js +65 -36
- package/src/convert/usd-to-gltf.js +8 -4
- package/src/gltf/document.js +107 -18
- package/src/gltf/glb.js +2 -2
- package/src/image/index.js +129 -1
- package/src/image/inflate.js +14 -12
- package/src/image/png.js +1 -1
- package/src/index.d.ts +92 -0
- package/src/index.js +33 -14
- package/src/limits.js +101 -0
- package/src/usd/stage.js +13 -9
- package/src/usd/usda/reader.js +17 -1
- package/src/usd/usda/writer.js +28 -7
- package/src/usd/usdc/reader.js +63 -13
- package/src/usd/usdc/writer.js +53 -3
- package/src/usd/usdz.js +9 -3
package/src/image/inflate.js
CHANGED
|
@@ -137,12 +137,12 @@ function corrupt( what ) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// Inflates the raw DEFLATE stream starting at src[ pos ]. Returns the output and the byte offset just past the stream.
|
|
140
|
-
function inflateStream( src, pos, sizeHint ) {
|
|
140
|
+
function inflateStream( src, pos, sizeHint, maxSize ) {
|
|
141
141
|
|
|
142
142
|
const srcLen = src.length;
|
|
143
143
|
const overrun = srcLen + 8;
|
|
144
144
|
|
|
145
|
-
let out = new Uint8Array( sizeHint > 0 ? sizeHint : Math.max( 1 << 15, srcLen * 4 ) );
|
|
145
|
+
let out = new Uint8Array( Math.min( sizeHint > 0 ? sizeHint : Math.max( 1 << 15, srcLen * 4 ), maxSize ) );
|
|
146
146
|
let op = 0;
|
|
147
147
|
|
|
148
148
|
let bitBuf = 0;
|
|
@@ -154,7 +154,7 @@ function inflateStream( src, pos, sizeHint ) {
|
|
|
154
154
|
let clTable = null;
|
|
155
155
|
|
|
156
156
|
let final = 0;
|
|
157
|
-
const state = { src, overrun, pos, bitBuf, bitCnt, out, op };
|
|
157
|
+
const state = { src, overrun, pos, bitBuf, bitCnt, out, op, maxSize };
|
|
158
158
|
|
|
159
159
|
do {
|
|
160
160
|
|
|
@@ -188,7 +188,7 @@ function inflateStream( src, pos, sizeHint ) {
|
|
|
188
188
|
pos += 4;
|
|
189
189
|
if ( pos + len > srcLen ) throw corrupt( 'unexpected end' );
|
|
190
190
|
|
|
191
|
-
if ( op + len > out.length ) out = grow( out, op + len );
|
|
191
|
+
if ( op + len > out.length ) out = grow( out, op + len, maxSize );
|
|
192
192
|
out.set( src.subarray( pos, pos + len ), op );
|
|
193
193
|
op += len;
|
|
194
194
|
pos += len;
|
|
@@ -371,7 +371,7 @@ function decodeSymbols( state, litTable, litMask, distTable, distMask ) {
|
|
|
371
371
|
|
|
372
372
|
if ( op === outLen ) {
|
|
373
373
|
|
|
374
|
-
out = grow( out, op + 1 );
|
|
374
|
+
out = grow( out, op + 1, state.maxSize );
|
|
375
375
|
outLen = out.length;
|
|
376
376
|
|
|
377
377
|
}
|
|
@@ -435,7 +435,7 @@ function decodeSymbols( state, litTable, litMask, distTable, distMask ) {
|
|
|
435
435
|
|
|
436
436
|
if ( op + len > outLen ) {
|
|
437
437
|
|
|
438
|
-
out = grow( out, op + len );
|
|
438
|
+
out = grow( out, op + len, state.maxSize );
|
|
439
439
|
outLen = out.length;
|
|
440
440
|
|
|
441
441
|
}
|
|
@@ -464,9 +464,10 @@ function decodeSymbols( state, litTable, litMask, distTable, distMask ) {
|
|
|
464
464
|
|
|
465
465
|
}
|
|
466
466
|
|
|
467
|
-
function grow( out, need ) {
|
|
467
|
+
function grow( out, need, maxSize ) {
|
|
468
468
|
|
|
469
|
-
|
|
469
|
+
if ( need > maxSize ) throw corrupt( `more than the expected ${ maxSize } bytes` );
|
|
470
|
+
const next = new Uint8Array( Math.min( Math.max( need, out.length * 2 ), maxSize ) );
|
|
470
471
|
next.set( out );
|
|
471
472
|
return next;
|
|
472
473
|
|
|
@@ -482,11 +483,12 @@ function toBytes( data ) {
|
|
|
482
483
|
* Decompresses raw DEFLATE data.
|
|
483
484
|
* @param {Uint8Array} bytes
|
|
484
485
|
* @param {number} [sizeHint] expected output size, avoids reallocation when known
|
|
486
|
+
* @param {number} [maxSize] fails rather than produce more than this
|
|
485
487
|
* @returns {Uint8Array}
|
|
486
488
|
*/
|
|
487
|
-
export function inflate( bytes, sizeHint = 0 ) {
|
|
489
|
+
export function inflate( bytes, sizeHint = 0, maxSize = Infinity ) {
|
|
488
490
|
|
|
489
|
-
return inflateStream( toBytes( bytes ), 0, sizeHint ).data;
|
|
491
|
+
return inflateStream( toBytes( bytes ), 0, sizeHint, maxSize ).data;
|
|
490
492
|
|
|
491
493
|
}
|
|
492
494
|
|
|
@@ -496,7 +498,7 @@ export function inflate( bytes, sizeHint = 0 ) {
|
|
|
496
498
|
* @param {number} [sizeHint]
|
|
497
499
|
* @returns {Uint8Array}
|
|
498
500
|
*/
|
|
499
|
-
export function inflateZlib( bytes, sizeHint = 0 ) {
|
|
501
|
+
export function inflateZlib( bytes, sizeHint = 0, maxSize = Infinity ) {
|
|
500
502
|
|
|
501
503
|
bytes = toBytes( bytes );
|
|
502
504
|
if ( bytes.length < 2 ) throw corrupt( 'missing zlib header' );
|
|
@@ -506,7 +508,7 @@ export function inflateZlib( bytes, sizeHint = 0 ) {
|
|
|
506
508
|
if ( ( cmf & 15 ) !== 8 || ( cmf >> 4 ) > 7 || ( ( cmf << 8 ) | flg ) % 31 !== 0 ) throw corrupt( 'bad zlib header' );
|
|
507
509
|
if ( flg & 32 ) throw corrupt( 'zlib preset dictionary not supported' );
|
|
508
510
|
|
|
509
|
-
const { data, end } = inflateStream( bytes, 2, sizeHint );
|
|
511
|
+
const { data, end } = inflateStream( bytes, 2, sizeHint, maxSize );
|
|
510
512
|
|
|
511
513
|
if ( end + 4 <= bytes.length ) {
|
|
512
514
|
|
package/src/image/png.js
CHANGED
|
@@ -219,7 +219,7 @@ export function decodePNG( bytes ) {
|
|
|
219
219
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
const raw = inflateZlib( zdata, expected );
|
|
222
|
+
const raw = inflateZlib( zdata, expected, expected * 2 + 1024 ); // tolerates trailing data, not a decompression bomb
|
|
223
223
|
if ( raw.length < expected ) throw new Error( 'usd-gltf: PNG image data is truncated' );
|
|
224
224
|
|
|
225
225
|
const channels = colorType === 3 ? ( trns !== null ? 4 : 3 ) : samples + ( trns !== null ? 1 : 0 );
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Loads a file referenced by a relative path or URI (glTF buffers and images, USD sublayers and references). */
|
|
2
|
+
export type Resolver = ( path: string ) => Uint8Array | ArrayBuffer | Promise<Uint8Array | ArrayBuffer>;
|
|
3
|
+
|
|
4
|
+
/** A decoded image: 8 or 16 bit samples, interleaved, top row first. */
|
|
5
|
+
export interface DecodedImage {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
channels: 1 | 2 | 3 | 4;
|
|
9
|
+
bitDepth: 8 | 16;
|
|
10
|
+
data: Uint8Array | Uint16Array;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Replaces the built in image decoding (the platform decoder in browsers, JavaScript decoders in Node). */
|
|
14
|
+
export type ImageDecoder = ( bytes: Uint8Array, mimeType?: string ) => DecodedImage | Promise<DecodedImage>;
|
|
15
|
+
|
|
16
|
+
/** Opt-in resource limits, for both directions. See README "Limits". */
|
|
17
|
+
export interface ConversionLimits {
|
|
18
|
+
/** Decoded data held during the conversion: accessors, USD arrays, decompressed data, images, arrays written to the output layer. */
|
|
19
|
+
maxDecodedBytes?: number;
|
|
20
|
+
/** Largest output buffer: checked as writers grow and before the final usdz or glb is allocated. */
|
|
21
|
+
maxOutputBytes?: number;
|
|
22
|
+
/** Cancels the conversion, which then rejects with the signal's reason. */
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Thrown when a conversion would go over one of its limits. Carries sizes only, never input data or file names. */
|
|
27
|
+
export class ConversionLimitError extends Error {
|
|
28
|
+
name: 'ConversionLimitError';
|
|
29
|
+
code: 'USD_GLTF_LIMIT';
|
|
30
|
+
/** Where the limit was reached, e.g. 'accessor 3', 'usdc', 'image', 'glb'. */
|
|
31
|
+
stage: string;
|
|
32
|
+
resource: 'decoded' | 'output';
|
|
33
|
+
limit: number;
|
|
34
|
+
attempted: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface USDToGLBOptions extends ConversionLimits {
|
|
38
|
+
/** Name of the input file, used to resolve relative paths. */
|
|
39
|
+
identifier?: string;
|
|
40
|
+
/** Needed when the USD references files outside a usdz package. */
|
|
41
|
+
resolver?: Resolver;
|
|
42
|
+
/** Variant selections, by prim path: { '/Root': { shadingVariant: 'red' } }, or by set name for every prim. */
|
|
43
|
+
variants?: Record<string, string | Record<string, string>>;
|
|
44
|
+
/** Write material variant sets as KHR_materials_variants (default true). */
|
|
45
|
+
materialVariants?: boolean;
|
|
46
|
+
imageDecoder?: ImageDecoder;
|
|
47
|
+
/** Receives non fatal problems, such as textures that could not be read. */
|
|
48
|
+
log?: ( message: string ) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface GLBToUSDOptions extends ConversionLimits {
|
|
52
|
+
/** Output encoding: a usdz package (default), or a single usda / usdc layer without its textures. */
|
|
53
|
+
format?: 'usdz' | 'usda' | 'usdc';
|
|
54
|
+
/** Needed for .gltf files with external buffers or images. */
|
|
55
|
+
resolver?: Resolver;
|
|
56
|
+
imageDecoder?: ImageDecoder;
|
|
57
|
+
log?: ( message: string ) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Converts USD (usda, usdc or usdz, detected from the bytes) to GLB. */
|
|
61
|
+
export function usdToGLB( data: Uint8Array | ArrayBuffer, options?: USDToGLBOptions ): Promise<Uint8Array>;
|
|
62
|
+
|
|
63
|
+
/** Converts glTF (GLB, or .gltf JSON with a resolver for its files) to USD. */
|
|
64
|
+
export function glbToUSD( data: Uint8Array | ArrayBuffer, options?: GLBToUSDOptions ): Promise<Uint8Array>;
|
|
65
|
+
|
|
66
|
+
/** Like glbToUSD for usda / usdc output, also returning the texture files the layer references. */
|
|
67
|
+
export function glbToUSDFiles( data: Uint8Array | ArrayBuffer, options?: Omit<GLBToUSDOptions, 'format'> & { format?: 'usda' | 'usdc' } ): Promise<{ layer: Uint8Array, files: Array<{ name: string, data: Uint8Array }> }>;
|
|
68
|
+
|
|
69
|
+
/** Identifies a file from its leading bytes. */
|
|
70
|
+
export function detectFormat( data: Uint8Array | ArrayBuffer ): 'glb' | 'gltf' | 'usda' | 'usdc' | 'usdz' | null;
|
|
71
|
+
|
|
72
|
+
/** Splits a GLB into its JSON and binary chunk. */
|
|
73
|
+
export function readGLB( data: Uint8Array | ArrayBuffer ): { json: any, bin: Uint8Array | null };
|
|
74
|
+
|
|
75
|
+
/** Packs glTF JSON (an object, or already UTF-8 encoded) and a binary buffer into a GLB. */
|
|
76
|
+
export function writeGLB( json: object | Uint8Array, bin?: Uint8Array | null ): Uint8Array;
|
|
77
|
+
|
|
78
|
+
/** The uncomposed contents of one USD layer; see src/usd/model.js for the value representation. */
|
|
79
|
+
export interface Layer {
|
|
80
|
+
identifier: string;
|
|
81
|
+
metadata: Record<string, any>;
|
|
82
|
+
root: any;
|
|
83
|
+
getPrimAtPath( path: string ): any;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function readUSDA( source: string | Uint8Array, identifier?: string ): Layer;
|
|
87
|
+
export function writeUSDA( layer: Layer ): string;
|
|
88
|
+
export function readUSDC( data: Uint8Array | ArrayBuffer, identifier?: string ): Layer;
|
|
89
|
+
export function writeUSDC( layer: Layer ): Uint8Array;
|
|
90
|
+
|
|
91
|
+
/** Opens and composes a USD stage (references, payloads, sublayers, inherits, variants). */
|
|
92
|
+
export function openStage( data: Uint8Array, options?: { identifier?: string, resolver?: Resolver, variants?: USDToGLBOptions[ 'variants' ] } ): Promise<any>;
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { readGLTF } from './gltf/document.js';
|
|
|
5
5
|
import { writeUSDA } from './usd/usda/writer.js';
|
|
6
6
|
import { writeUSDC } from './usd/usdc/writer.js';
|
|
7
7
|
import { writeUSDZ } from './usd/usdz.js';
|
|
8
|
+
import { Budget } from './limits.js';
|
|
8
9
|
|
|
9
10
|
export { readGLB, writeGLB } from './gltf/glb.js';
|
|
10
11
|
export { detectFormat } from './detect.js';
|
|
@@ -13,6 +14,7 @@ export { readUSDA } from './usd/usda/reader.js';
|
|
|
13
14
|
export { writeUSDA } from './usd/usda/writer.js';
|
|
14
15
|
export { readUSDC } from './usd/usdc/reader.js';
|
|
15
16
|
export { writeUSDC } from './usd/usdc/writer.js';
|
|
17
|
+
export { ConversionLimitError } from './limits.js';
|
|
16
18
|
|
|
17
19
|
function toBytes( data ) {
|
|
18
20
|
|
|
@@ -20,37 +22,50 @@ function toBytes( data ) {
|
|
|
20
22
|
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
// Limits shared by both directions; see README "Limits".
|
|
26
|
+
function budgetFor( { maxDecodedBytes, maxOutputBytes, signal } ) {
|
|
27
|
+
|
|
28
|
+
return new Budget( { maxDecodedBytes, maxOutputBytes, signal } );
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
/**
|
|
24
33
|
* Converts USD (usda, usdc or usdz, detected from the bytes) to GLB.
|
|
25
34
|
* @param {ArrayBuffer|Uint8Array} data
|
|
26
|
-
* @param {{ identifier?: string, resolver?: ( path: string ) => Promise<Uint8Array>, variants?: object, log?: ( message: string ) => void }} [options]
|
|
35
|
+
* @param {{ identifier?: string, resolver?: ( path: string ) => Promise<Uint8Array>, variants?: object, log?: ( message: string ) => void, maxDecodedBytes?: number, maxOutputBytes?: number, signal?: AbortSignal }} [options]
|
|
27
36
|
* resolver loads files referenced by relative paths (not needed for self-contained usdz)
|
|
28
37
|
* @returns {Promise<Uint8Array>}
|
|
29
38
|
*/
|
|
30
39
|
export async function usdToGLB( data, options = {} ) {
|
|
31
40
|
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
41
|
+
const budget = budgetFor( options );
|
|
42
|
+
const stage = await openStage( toBytes( data ), { ...options, budget } );
|
|
43
|
+
const builder = await new USDToGLTF( stage, { ...options, budget } ).convert();
|
|
44
|
+
const glb = builder.toGLB();
|
|
45
|
+
budget.check();
|
|
46
|
+
return glb;
|
|
35
47
|
|
|
36
48
|
}
|
|
37
49
|
|
|
38
50
|
/**
|
|
39
51
|
* Converts GLB (or glTF JSON with embedded or resolvable buffers) to USD.
|
|
40
52
|
* @param {ArrayBuffer|Uint8Array} data
|
|
41
|
-
* @param {{ format?: 'usda'|'usdc'|'usdz', resolver?: ( uri: string ) => Promise<Uint8Array
|
|
53
|
+
* @param {{ format?: 'usda'|'usdc'|'usdz', resolver?: ( uri: string ) => Promise<Uint8Array>, maxDecodedBytes?: number, maxOutputBytes?: number, signal?: AbortSignal }} [options]
|
|
42
54
|
* @returns {Promise<Uint8Array>}
|
|
43
55
|
*/
|
|
44
|
-
export async function glbToUSD( data, { format = 'usdz', resolver = null, log, imageDecoder } = {} ) {
|
|
56
|
+
export async function glbToUSD( data, { format = 'usdz', resolver = null, log, imageDecoder, ...limits } = {} ) {
|
|
45
57
|
|
|
46
|
-
const
|
|
58
|
+
const budget = budgetFor( limits );
|
|
59
|
+
const doc = await readGLTF( toBytes( data ), { resolver, budget } );
|
|
47
60
|
const { layer, files } = await new GLTFToUSD( doc, { log, imageDecoder } ).convert();
|
|
61
|
+
let out;
|
|
48
62
|
|
|
49
|
-
if ( format === 'usda' )
|
|
50
|
-
if ( format === 'usdc' )
|
|
63
|
+
if ( format === 'usda' ) out = new TextEncoder().encode( writeUSDA( layer, { budget } ) );
|
|
64
|
+
else if ( format === 'usdc' ) out = writeUSDC( layer, { budget } );
|
|
65
|
+
else out = writeUSDZ( [ { name: 'root.usdc', data: writeUSDC( layer, { budget } ) }, ...files ], { budget } ); // the root layer must come first
|
|
51
66
|
|
|
52
|
-
|
|
53
|
-
return
|
|
67
|
+
budget.check();
|
|
68
|
+
return out;
|
|
54
69
|
|
|
55
70
|
}
|
|
56
71
|
|
|
@@ -58,10 +73,14 @@ export async function glbToUSD( data, { format = 'usdz', resolver = null, log, i
|
|
|
58
73
|
* For usda / usdc output the textures are separate files; this returns them alongside the layer.
|
|
59
74
|
* @returns {Promise<{ layer: Uint8Array, files: Array<{ name: string, data: Uint8Array }> }>}
|
|
60
75
|
*/
|
|
61
|
-
export async function glbToUSDFiles( data, { format = 'usda', resolver = null, log, imageDecoder } = {} ) {
|
|
76
|
+
export async function glbToUSDFiles( data, { format = 'usda', resolver = null, log, imageDecoder, ...limits } = {} ) {
|
|
62
77
|
|
|
63
|
-
const
|
|
78
|
+
const budget = budgetFor( limits );
|
|
79
|
+
const doc = await readGLTF( toBytes( data ), { resolver, budget } );
|
|
64
80
|
const { layer, files } = await new GLTFToUSD( doc, { log, imageDecoder } ).convert();
|
|
65
|
-
|
|
81
|
+
const bytes = format === 'usdc' ? writeUSDC( layer, { budget } ) : new TextEncoder().encode( writeUSDA( layer, { budget } ) );
|
|
82
|
+
budget.output( files.reduce( ( n, f ) => n + f.data.byteLength, bytes.byteLength ), 'usd files' );
|
|
83
|
+
budget.check();
|
|
84
|
+
return { layer: bytes, files };
|
|
66
85
|
|
|
67
86
|
}
|
package/src/limits.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Opt-in resource limits for a conversion: decoded data, serialized output and cancellation.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thrown when a conversion would go over one of its limits. The message and fields describe sizes
|
|
5
|
+
* only, never input data or file names.
|
|
6
|
+
*/
|
|
7
|
+
export class ConversionLimitError extends Error {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {{ stage: string, resource: 'decoded'|'output', limit: number, attempted: number }} info
|
|
11
|
+
*/
|
|
12
|
+
constructor( { stage, resource, limit, attempted } ) {
|
|
13
|
+
|
|
14
|
+
super( `usd-gltf: ${ stage } needs ${ attempted } ${ resource } bytes, over the limit of ${ limit }` );
|
|
15
|
+
this.name = 'ConversionLimitError';
|
|
16
|
+
this.code = 'USD_GLTF_LIMIT';
|
|
17
|
+
this.stage = stage;
|
|
18
|
+
this.resource = resource;
|
|
19
|
+
this.limit = limit;
|
|
20
|
+
this.attempted = attempted;
|
|
21
|
+
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Tracks one conversion against its limits. Readers and converters charge decoded bytes before
|
|
28
|
+
* allocating them; writers report their size before growing.
|
|
29
|
+
*/
|
|
30
|
+
export class Budget {
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {{ maxDecodedBytes?: number, maxOutputBytes?: number, signal?: AbortSignal }} [options]
|
|
34
|
+
*/
|
|
35
|
+
constructor( { maxDecodedBytes = Infinity, maxOutputBytes = Infinity, signal = null } = {} ) {
|
|
36
|
+
|
|
37
|
+
this.maxDecodedBytes = maxDecodedBytes;
|
|
38
|
+
this.maxOutputBytes = maxOutputBytes;
|
|
39
|
+
this.signal = signal;
|
|
40
|
+
this.decodedBytes = 0;
|
|
41
|
+
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Adds to the running total of decoded data: accessors, arrays, decompressed sections and images.
|
|
46
|
+
* @param {number} bytes
|
|
47
|
+
* @param {string} stage
|
|
48
|
+
*/
|
|
49
|
+
decode( bytes, stage ) {
|
|
50
|
+
|
|
51
|
+
checkSize( bytes, stage );
|
|
52
|
+
this.check();
|
|
53
|
+
this.decodedBytes += bytes;
|
|
54
|
+
if ( this.decodedBytes > this.maxDecodedBytes ) throw new ConversionLimitError( { stage, resource: 'decoded', limit: this.maxDecodedBytes, attempted: this.decodedBytes } );
|
|
55
|
+
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Checks the size an output buffer is about to reach.
|
|
60
|
+
* @param {number} bytes
|
|
61
|
+
* @param {string} stage
|
|
62
|
+
*/
|
|
63
|
+
output( bytes, stage ) {
|
|
64
|
+
|
|
65
|
+
checkSize( bytes, stage );
|
|
66
|
+
if ( bytes > this.maxOutputBytes ) throw new ConversionLimitError( { stage, resource: 'output', limit: this.maxOutputBytes, attempted: bytes } );
|
|
67
|
+
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Throws the signal's reason once the conversion is cancelled.
|
|
71
|
+
check() {
|
|
72
|
+
|
|
73
|
+
this.signal?.throwIfAborted();
|
|
74
|
+
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A size computed from counts in a file must be a whole number of bytes that fits in an array.
|
|
81
|
+
* @param {number} bytes
|
|
82
|
+
* @param {string} stage
|
|
83
|
+
*/
|
|
84
|
+
export function checkSize( bytes, stage ) {
|
|
85
|
+
|
|
86
|
+
if ( ! Number.isSafeInteger( bytes ) || bytes < 0 ) throw new Error( `usd-gltf: invalid size in ${ stage }` );
|
|
87
|
+
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* For catch blocks that skip one failed asset and carry on: rethrows what must end the whole
|
|
92
|
+
* conversion instead, a limit error or cancellation.
|
|
93
|
+
* @param {unknown} error
|
|
94
|
+
* @param {Budget} [budget]
|
|
95
|
+
*/
|
|
96
|
+
export function rethrowFatal( error, budget ) {
|
|
97
|
+
|
|
98
|
+
budget?.check();
|
|
99
|
+
if ( error instanceof ConversionLimitError ) throw error;
|
|
100
|
+
|
|
101
|
+
}
|
package/src/usd/stage.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { AssetPath, ListOp } from './model.js';
|
|
6
6
|
import { readUSDA } from './usda/reader.js';
|
|
7
7
|
import { readZip } from './usdz.js';
|
|
8
|
+
import { Budget, rethrowFatal } from '../limits.js';
|
|
8
9
|
|
|
9
10
|
const decoder = new TextDecoder();
|
|
10
11
|
|
|
@@ -80,9 +81,10 @@ export function resolveAssetPath( anchor, assetPath ) {
|
|
|
80
81
|
// Reads bytes for identifiers, opening packages (including nested ones) through the user resolver.
|
|
81
82
|
export class AssetReader {
|
|
82
83
|
|
|
83
|
-
constructor( resolver ) {
|
|
84
|
+
constructor( resolver, budget = new Budget() ) {
|
|
84
85
|
|
|
85
86
|
this.resolver = resolver;
|
|
87
|
+
this.budget = budget;
|
|
86
88
|
this.packages = new Map();
|
|
87
89
|
this.files = new Map();
|
|
88
90
|
|
|
@@ -123,7 +125,7 @@ export class AssetReader {
|
|
|
123
125
|
|
|
124
126
|
async openPackage( identifier ) {
|
|
125
127
|
|
|
126
|
-
if ( ! this.packages.has( identifier ) ) this.packages.set( identifier, readZip( await this.read( identifier ) ) );
|
|
128
|
+
if ( ! this.packages.has( identifier ) ) this.packages.set( identifier, readZip( await this.read( identifier ), { budget: this.budget } ) );
|
|
127
129
|
return this.packages.get( identifier );
|
|
128
130
|
|
|
129
131
|
}
|
|
@@ -140,7 +142,7 @@ export class AssetReader {
|
|
|
140
142
|
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
async function parseLayer( bytes, identifier ) {
|
|
145
|
+
async function parseLayer( bytes, identifier, budget ) {
|
|
144
146
|
|
|
145
147
|
const format = detectLayerFormat( bytes );
|
|
146
148
|
if ( format === 'usda' ) return readUSDA( bytes, identifier );
|
|
@@ -148,7 +150,7 @@ async function parseLayer( bytes, identifier ) {
|
|
|
148
150
|
if ( format === 'usdc' ) {
|
|
149
151
|
|
|
150
152
|
const { readUSDC } = await import( './usdc/reader.js' );
|
|
151
|
-
return readUSDC( bytes, identifier );
|
|
153
|
+
return readUSDC( bytes, identifier, { budget } );
|
|
152
154
|
|
|
153
155
|
}
|
|
154
156
|
|
|
@@ -185,14 +187,15 @@ async function loadLayers( reader, rootIdentifier ) {
|
|
|
185
187
|
|
|
186
188
|
if ( detectLayerFormat( bytes ) === 'usdz' ) {
|
|
187
189
|
|
|
188
|
-
reader.addPackage( identifier, readZip( bytes ) );
|
|
190
|
+
reader.addPackage( identifier, readZip( bytes, { budget: reader.budget } ) );
|
|
189
191
|
const root = await load( await reader.packageRoot( identifier ) );
|
|
190
192
|
layers.set( identifier, root );
|
|
191
193
|
return root;
|
|
192
194
|
|
|
193
195
|
}
|
|
194
196
|
|
|
195
|
-
|
|
197
|
+
reader.budget.check();
|
|
198
|
+
const layer = await parseLayer( bytes, identifier, reader.budget );
|
|
196
199
|
layers.set( identifier, layer );
|
|
197
200
|
|
|
198
201
|
const pending = [];
|
|
@@ -216,6 +219,7 @@ async function loadLayers( reader, rootIdentifier ) {
|
|
|
216
219
|
|
|
217
220
|
} catch ( e ) {
|
|
218
221
|
|
|
222
|
+
rethrowFatal( e, reader.budget );
|
|
219
223
|
console.warn( `usd-gltf: ${ e.message }` );
|
|
220
224
|
|
|
221
225
|
}
|
|
@@ -862,16 +866,16 @@ export class Stage {
|
|
|
862
866
|
/**
|
|
863
867
|
* Opens and composes a stage.
|
|
864
868
|
* @param {Uint8Array} bytes usda, usdc or usdz
|
|
865
|
-
* @param {{ identifier?: string, resolver?: ( identifier: string ) => Promise<Uint8Array>|Uint8Array, variants?: object }} [options]
|
|
869
|
+
* @param {{ identifier?: string, resolver?: ( identifier: string ) => Promise<Uint8Array>|Uint8Array, variants?: object, budget?: import('../limits.js').Budget }} [options]
|
|
866
870
|
* @returns {Promise<Stage>}
|
|
867
871
|
*/
|
|
868
|
-
export async function openStage( bytes, { identifier, resolver = null, variants = {} } = {} ) {
|
|
872
|
+
export async function openStage( bytes, { identifier, resolver = null, variants = {}, budget = new Budget() } = {} ) {
|
|
869
873
|
|
|
870
874
|
const format = detectLayerFormat( bytes );
|
|
871
875
|
if ( ! format ) throw new Error( 'usd-gltf: input is not usda, usdc or usdz' );
|
|
872
876
|
|
|
873
877
|
identifier ??= 'root.' + format;
|
|
874
|
-
const reader = new AssetReader( resolver );
|
|
878
|
+
const reader = new AssetReader( resolver, budget );
|
|
875
879
|
reader.files.set( identifier, bytes );
|
|
876
880
|
|
|
877
881
|
const { root, layers } = await loadLayers( reader, identifier );
|
package/src/usd/usda/reader.js
CHANGED
|
@@ -358,7 +358,7 @@ function typeValue( raw, typeName ) {
|
|
|
358
358
|
|
|
359
359
|
if ( typeName === 'asset' ) return new AssetPath( raw instanceof AssetRef ? raw.path : String( raw ) );
|
|
360
360
|
if ( typeName === 'bool' ) return raw === true || raw === 1;
|
|
361
|
-
if ( typeName === 'string' || typeName === 'token' ) return raw instanceof Ident ? raw.name : raw;
|
|
361
|
+
if ( typeName === 'string' || typeName === 'token' || typeName === 'pathExpression' ) return raw instanceof Ident ? raw.name : raw;
|
|
362
362
|
if ( /^quat[hfd]$/.test( typeName ) ) return [ raw[ 1 ], raw[ 2 ], raw[ 3 ], raw[ 0 ] ];
|
|
363
363
|
if ( /^matrix[234]d$|^frame4d$/.test( typeName ) ) return raw.flat();
|
|
364
364
|
if ( raw instanceof Tuple ) return Array.from( raw );
|
|
@@ -524,6 +524,22 @@ function parseMetadata( lx, metadata, kind ) {
|
|
|
524
524
|
metadata.subLayers = layers.map( ( r ) => r.assetPath );
|
|
525
525
|
metadata.subLayerOffsets = layers.map( ( r ) => r.layerOffset );
|
|
526
526
|
|
|
527
|
+
} else if ( key === 'relocates' ) {
|
|
528
|
+
|
|
529
|
+
// { <source>: <target>, ... }; layer relocates use the layerRelocates field, as in usdc
|
|
530
|
+
const pairs = [];
|
|
531
|
+
lx.expect( '{' );
|
|
532
|
+
while ( ! lx.eat( '}' ) ) {
|
|
533
|
+
|
|
534
|
+
const from = lx.path();
|
|
535
|
+
lx.expect( ':' );
|
|
536
|
+
pairs.push( [ from, lx.path() ] );
|
|
537
|
+
lx.eat( ',' );
|
|
538
|
+
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
metadata[ kind === 'layer' ? 'layerRelocates' : 'relocates' ] = pairs;
|
|
542
|
+
|
|
527
543
|
} else if ( key === 'variants' ) {
|
|
528
544
|
|
|
529
545
|
metadata.variants = parseDictionary( lx );
|
package/src/usd/usda/writer.js
CHANGED
|
@@ -56,7 +56,7 @@ function formatScalar( v, base ) {
|
|
|
56
56
|
|
|
57
57
|
if ( v === null || v === undefined ) return 'None';
|
|
58
58
|
if ( base === 'bool' ) return v ? 'true' : 'false';
|
|
59
|
-
if ( base === 'string' || base === 'token' ) return quote( String( v ) );
|
|
59
|
+
if ( base === 'string' || base === 'token' || base === 'pathExpression' ) return quote( String( v ) );
|
|
60
60
|
if ( base === 'asset' ) return formatAsset( v );
|
|
61
61
|
if ( base === 'dictionary' ) return formatDictionary( v, '' );
|
|
62
62
|
|
|
@@ -142,12 +142,12 @@ function formatDictionary( dict, indent ) {
|
|
|
142
142
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
function formatLayerOffset( offset ) {
|
|
145
|
+
function formatLayerOffset( offset, customData = null, indent = '' ) {
|
|
146
146
|
|
|
147
|
-
if ( ! offset ) return '';
|
|
148
147
|
const parts = [];
|
|
149
|
-
if ( offset
|
|
150
|
-
if ( offset
|
|
148
|
+
if ( offset?.offset ) parts.push( `offset = ${ offset.offset }` );
|
|
149
|
+
if ( offset?.scale !== undefined && offset.scale !== 1 ) parts.push( `scale = ${ offset.scale }` );
|
|
150
|
+
if ( customData && Object.keys( customData ).length ) parts.push( `customData = ${ formatDictionary( customData, indent ) }` );
|
|
151
151
|
return parts.length ? ` (${ parts.join( '; ' ) })` : '';
|
|
152
152
|
|
|
153
153
|
}
|
|
@@ -157,7 +157,7 @@ function formatReference( r ) {
|
|
|
157
157
|
let s = '';
|
|
158
158
|
if ( r.assetPath ) s += formatAsset( r.assetPath );
|
|
159
159
|
if ( r.primPath ) s += `<${ r.primPath }>`;
|
|
160
|
-
return s + formatLayerOffset( r.layerOffset );
|
|
160
|
+
return s + formatLayerOffset( r.layerOffset, r.customData );
|
|
161
161
|
|
|
162
162
|
}
|
|
163
163
|
|
|
@@ -218,6 +218,11 @@ function formatMetadataEntries( metadata, indent, kind ) {
|
|
|
218
218
|
const offsets = metadata.subLayerOffsets || [];
|
|
219
219
|
lines.push( `${ indent }subLayers = [\n${ value.map( ( p, i ) => `${ indent } ${ formatAsset( p ) }${ formatLayerOffset( offsets[ i ] ) }` ).join( ',\n' ) }\n${ indent }]` );
|
|
220
220
|
|
|
221
|
+
} else if ( key === 'layerRelocates' || key === 'relocates' ) {
|
|
222
|
+
|
|
223
|
+
const pairs = value.map( ( [ a, b ] ) => `${ indent } <${ a }>: <${ b }>` );
|
|
224
|
+
lines.push( `${ indent }relocates = {\n${ pairs.join( ',\n' ) }\n${ indent }}` );
|
|
225
|
+
|
|
221
226
|
} else if ( key === 'subLayerOffsets' ) {
|
|
222
227
|
|
|
223
228
|
continue;
|
|
@@ -309,6 +314,7 @@ function writePrimContents( out, spec, indent ) {
|
|
|
309
314
|
|
|
310
315
|
for ( const prop of spec.properties.values() ) writeProperty( out, prop, indent );
|
|
311
316
|
|
|
317
|
+
if ( spec.metadata.propertyOrder ) out.push( `${ indent }reorder properties = [${ spec.metadata.propertyOrder.map( quote ).join( ', ' ) }]` );
|
|
312
318
|
if ( spec.metadata.primOrder ) out.push( `${ indent }reorder nameChildren = [${ spec.metadata.primOrder.map( quote ).join( ', ' ) }]` );
|
|
313
319
|
|
|
314
320
|
for ( const child of spec.children.values() ) {
|
|
@@ -351,11 +357,26 @@ function writePrim( out, spec, indent ) {
|
|
|
351
357
|
|
|
352
358
|
/**
|
|
353
359
|
* @param {import('../model.js').Layer} layer
|
|
360
|
+
* @param {{ budget?: import('../../limits.js').Budget }} [options] its output limit is checked as lines are written
|
|
354
361
|
* @returns {string}
|
|
355
362
|
*/
|
|
356
|
-
export function writeUSDA( layer ) {
|
|
363
|
+
export function writeUSDA( layer, { budget = null } = {} ) {
|
|
357
364
|
|
|
358
365
|
const out = [ '#usda 1.0' ];
|
|
366
|
+
|
|
367
|
+
if ( budget ) {
|
|
368
|
+
|
|
369
|
+
let size = 0;
|
|
370
|
+
const push = out.push;
|
|
371
|
+
out.push = function ( ...lines ) {
|
|
372
|
+
|
|
373
|
+
for ( const line of lines ) size += line.length + 1;
|
|
374
|
+
budget.output( size, 'usda' );
|
|
375
|
+
return push.apply( this, lines );
|
|
376
|
+
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
}
|
|
359
380
|
const meta = formatMetadataEntries( layer.metadata, ' ', 'layer' );
|
|
360
381
|
if ( meta.length ) out.push( '(', ...meta, ')' );
|
|
361
382
|
out.push( '' );
|