usd-gltf 0.1.0 → 0.3.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 +36 -3
- package/package.json +12 -2
- package/src/convert/gltf-to-usd.js +166 -46
- package/src/convert/usd-material.js +7 -2
- package/src/convert/usd-to-gltf.js +8 -4
- package/src/gltf/document.js +109 -19
- package/src/gltf/draco.js +191 -0
- package/src/gltf/glb.js +2 -2
- package/src/image/index.js +52 -1
- package/src/image/inflate.js +14 -12
- package/src/image/png.js +1 -1
- package/src/index.d.ts +30 -4
- package/src/index.js +33 -14
- package/src/limits.js +101 -0
- package/src/usd/stage.js +13 -9
- package/src/usd/usda/writer.js +16 -1
- 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/index.d.ts
CHANGED
|
@@ -13,7 +13,28 @@ export interface DecodedImage {
|
|
|
13
13
|
/** Replaces the built in image decoding (the platform decoder in browsers, JavaScript decoders in Node). */
|
|
14
14
|
export type ImageDecoder = ( bytes: Uint8Array, mimeType?: string ) => DecodedImage | Promise<DecodedImage>;
|
|
15
15
|
|
|
16
|
-
|
|
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 {
|
|
17
38
|
/** Name of the input file, used to resolve relative paths. */
|
|
18
39
|
identifier?: string;
|
|
19
40
|
/** Needed when the USD references files outside a usdz package. */
|
|
@@ -27,11 +48,16 @@ export interface USDToGLBOptions {
|
|
|
27
48
|
log?: ( message: string ) => void;
|
|
28
49
|
}
|
|
29
50
|
|
|
30
|
-
export interface GLBToUSDOptions {
|
|
51
|
+
export interface GLBToUSDOptions extends ConversionLimits {
|
|
31
52
|
/** Output encoding: a usdz package (default), or a single usda / usdc layer without its textures. */
|
|
32
53
|
format?: 'usdz' | 'usda' | 'usdc';
|
|
33
54
|
/** Needed for .gltf files with external buffers or images. */
|
|
34
55
|
resolver?: Resolver;
|
|
56
|
+
/**
|
|
57
|
+
* A Draco decoder module (from draco3d's createDecoderModule, or Google's decoder build), or a
|
|
58
|
+
* promise of one. Without it, Draco files load draco3d in Node and Google's CDN build in browsers.
|
|
59
|
+
*/
|
|
60
|
+
dracoDecoder?: object | Promise<object>;
|
|
35
61
|
imageDecoder?: ImageDecoder;
|
|
36
62
|
log?: ( message: string ) => void;
|
|
37
63
|
}
|
|
@@ -51,8 +77,8 @@ export function detectFormat( data: Uint8Array | ArrayBuffer ): 'glb' | 'gltf' |
|
|
|
51
77
|
/** Splits a GLB into its JSON and binary chunk. */
|
|
52
78
|
export function readGLB( data: Uint8Array | ArrayBuffer ): { json: any, bin: Uint8Array | null };
|
|
53
79
|
|
|
54
|
-
/** Packs glTF JSON and a binary buffer into a GLB. */
|
|
55
|
-
export function writeGLB( json: object, bin?: Uint8Array | null ): Uint8Array;
|
|
80
|
+
/** Packs glTF JSON (an object, or already UTF-8 encoded) and a binary buffer into a GLB. */
|
|
81
|
+
export function writeGLB( json: object | Uint8Array, bin?: Uint8Array | null ): Uint8Array;
|
|
56
82
|
|
|
57
83
|
/** The uncomposed contents of one USD layer; see src/usd/model.js for the value representation. */
|
|
58
84
|
export interface Layer {
|
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>, dracoDecoder?: object, 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, dracoDecoder, ...limits } = {} ) {
|
|
45
57
|
|
|
46
|
-
const
|
|
58
|
+
const budget = budgetFor( limits );
|
|
59
|
+
const doc = await readGLTF( toBytes( data ), { resolver, budget, dracoDecoder } );
|
|
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, dracoDecoder, ...limits } = {} ) {
|
|
62
77
|
|
|
63
|
-
const
|
|
78
|
+
const budget = budgetFor( limits );
|
|
79
|
+
const doc = await readGLTF( toBytes( data ), { resolver, budget, dracoDecoder } );
|
|
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/writer.js
CHANGED
|
@@ -357,11 +357,26 @@ function writePrim( out, spec, indent ) {
|
|
|
357
357
|
|
|
358
358
|
/**
|
|
359
359
|
* @param {import('../model.js').Layer} layer
|
|
360
|
+
* @param {{ budget?: import('../../limits.js').Budget }} [options] its output limit is checked as lines are written
|
|
360
361
|
* @returns {string}
|
|
361
362
|
*/
|
|
362
|
-
export function writeUSDA( layer ) {
|
|
363
|
+
export function writeUSDA( layer, { budget = null } = {} ) {
|
|
363
364
|
|
|
364
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
|
+
}
|
|
365
380
|
const meta = formatMetadataEntries( layer.metadata, ' ', 'layer' );
|
|
366
381
|
if ( meta.length ) out.push( '(', ...meta, ')' );
|
|
367
382
|
out.push( '' );
|
package/src/usd/usdc/reader.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { AssetPath, DICT_TYPES, Layer, ListOp, PrimSpec, PropertySpec, Reference } from '../model.js';
|
|
5
5
|
import { decompressFast } from './lz4.js';
|
|
6
6
|
import { decompressInts32, decompressInts64 } from './intcompression.js';
|
|
7
|
+
import { Budget, checkSize } from '../../limits.js';
|
|
7
8
|
import { PERMISSIONS, POD, SCALAR_SIZE, SPEC, SPECIFIERS, TYPE, TYPE_NAMES, halfArrayToFloat32, halfToFloat, isMatrixType, isVecType } from './types.js';
|
|
8
9
|
|
|
9
10
|
const IS_ARRAY = 0x80000000;
|
|
@@ -42,9 +43,10 @@ const V0_8_0 = version( 0, 8, 0 );
|
|
|
42
43
|
|
|
43
44
|
class CrateReader {
|
|
44
45
|
|
|
45
|
-
constructor( bytes ) {
|
|
46
|
+
constructor( bytes, budget = new Budget() ) {
|
|
46
47
|
|
|
47
48
|
this.bytes = bytes;
|
|
49
|
+
this.budget = budget;
|
|
48
50
|
this.view = new DataView( bytes.buffer, bytes.byteOffset, bytes.byteLength );
|
|
49
51
|
this.pos = 0;
|
|
50
52
|
this.timesCache = new Map();
|
|
@@ -136,7 +138,7 @@ class CrateReader {
|
|
|
136
138
|
|
|
137
139
|
take( n ) {
|
|
138
140
|
|
|
139
|
-
if ( this.pos + n > this.bytes.length ) throw new Error( 'usd-gltf: truncated usdc file' );
|
|
141
|
+
if ( ! Number.isSafeInteger( n ) || n < 0 || this.pos + n > this.bytes.length ) throw new Error( 'usd-gltf: truncated usdc file' );
|
|
140
142
|
const b = this.bytes.subarray( this.pos, this.pos + n );
|
|
141
143
|
this.pos += n;
|
|
142
144
|
return b;
|
|
@@ -145,8 +147,37 @@ class CrateReader {
|
|
|
145
147
|
|
|
146
148
|
compressedInts( n ) {
|
|
147
149
|
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
+
const data = this.take( this.u64() );
|
|
151
|
+
this.decodedInts( n, 4, data );
|
|
152
|
+
return decompressInts32( data, n );
|
|
153
|
+
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Sizes come from the file, so they are checked before allocating. Compressed integers take at
|
|
157
|
+
// least 2 bits each before LZ4, which shrinks data at most 255 times.
|
|
158
|
+
decodedInts( n, size, data ) {
|
|
159
|
+
|
|
160
|
+
checkSize( n * size, 'usdc' );
|
|
161
|
+
if ( n > data.length * 1024 + 64 ) throw new Error( 'usd-gltf: corrupt compressed integers in usdc file' );
|
|
162
|
+
this.budget.decode( n * size * 2, 'usdc' ); // the encoded buffer and the decoded integers
|
|
163
|
+
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
lz4( data, size ) {
|
|
167
|
+
|
|
168
|
+
checkSize( size, 'usdc' );
|
|
169
|
+
if ( size > data.length * 256 + 64 ) throw new Error( 'usd-gltf: corrupt compressed data in usdc file' );
|
|
170
|
+
this.budget.decode( size, 'usdc' );
|
|
171
|
+
return decompressFast( data, size );
|
|
172
|
+
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// count fixed size records at this.pos, which must all be in the file
|
|
176
|
+
records( count, size ) {
|
|
177
|
+
|
|
178
|
+
checkSize( count * size, 'usdc' );
|
|
179
|
+
if ( this.pos + count * size > this.bytes.length ) throw new Error( 'usd-gltf: truncated usdc file' );
|
|
180
|
+
this.budget.decode( count * size, 'usdc' );
|
|
150
181
|
|
|
151
182
|
}
|
|
152
183
|
|
|
@@ -176,7 +207,7 @@ class CrateReader {
|
|
|
176
207
|
|
|
177
208
|
const uncompressedSize = this.u64();
|
|
178
209
|
const compressedSize = this.u64();
|
|
179
|
-
chars =
|
|
210
|
+
chars = this.lz4( this.take( compressedSize ), uncompressedSize );
|
|
180
211
|
|
|
181
212
|
}
|
|
182
213
|
|
|
@@ -213,6 +244,7 @@ class CrateReader {
|
|
|
213
244
|
|
|
214
245
|
if ( this.version < V0_4_0 ) {
|
|
215
246
|
|
|
247
|
+
this.records( count, 16 );
|
|
216
248
|
this.fieldTokens = new Uint32Array( count );
|
|
217
249
|
this.fieldReps = new Uint32Array( count * 2 );
|
|
218
250
|
|
|
@@ -229,7 +261,7 @@ class CrateReader {
|
|
|
229
261
|
|
|
230
262
|
this.fieldTokens = new Uint32Array( this.compressedInts( count ).buffer );
|
|
231
263
|
const repsSize = this.u64();
|
|
232
|
-
const reps =
|
|
264
|
+
const reps = this.lz4( this.take( repsSize ), count * 8 );
|
|
233
265
|
this.fieldReps = new Uint32Array( reps.buffer, 0, count * 2 );
|
|
234
266
|
|
|
235
267
|
}
|
|
@@ -245,6 +277,7 @@ class CrateReader {
|
|
|
245
277
|
|
|
246
278
|
if ( this.version < V0_4_0 ) {
|
|
247
279
|
|
|
280
|
+
this.records( count, 4 );
|
|
248
281
|
this.fieldSets = new Uint32Array( count );
|
|
249
282
|
for ( let i = 0; i < count; i ++ ) this.fieldSets[ i ] = this.u32();
|
|
250
283
|
|
|
@@ -263,16 +296,27 @@ class CrateReader {
|
|
|
263
296
|
if ( ! this.section( 'PATHS' ) ) return;
|
|
264
297
|
|
|
265
298
|
const numPaths = this.u64();
|
|
266
|
-
|
|
267
|
-
|
|
299
|
+
const allocate = () => {
|
|
300
|
+
|
|
301
|
+
this.budget.decode( numPaths * 16, 'usdc' );
|
|
302
|
+
this.paths = new Array( numPaths ).fill( '' );
|
|
303
|
+
this.pathInfo = new Array( numPaths ).fill( null );
|
|
304
|
+
|
|
305
|
+
};
|
|
268
306
|
|
|
269
307
|
if ( this.version < V0_4_0 ) {
|
|
270
308
|
|
|
271
|
-
|
|
309
|
+
const entry = this.version === version( 0, 0, 1 ) ? 16 : 12;
|
|
310
|
+
this.records( numPaths, entry ); // a tree entry per path
|
|
311
|
+
allocate();
|
|
312
|
+
this.readPathTree( entry );
|
|
272
313
|
|
|
273
314
|
} else {
|
|
274
315
|
|
|
275
316
|
const n = this.u64();
|
|
317
|
+
// the tree holds every path, sometimes leaving out an unused first slot
|
|
318
|
+
if ( ! Number.isSafeInteger( numPaths ) || numPaths > n + 1 ) throw new Error( 'usd-gltf: corrupt path table in usdc file' );
|
|
319
|
+
allocate();
|
|
276
320
|
const pathIndexes = this.compressedInts( n );
|
|
277
321
|
const elementTokens = this.compressedInts( n );
|
|
278
322
|
const jumps = this.compressedInts( n );
|
|
@@ -405,6 +449,7 @@ class CrateReader {
|
|
|
405
449
|
if ( this.version < V0_4_0 ) {
|
|
406
450
|
|
|
407
451
|
const pad = this.version === version( 0, 0, 1 ) ? 4 : 0;
|
|
452
|
+
this.records( count, 12 + pad );
|
|
408
453
|
this.specPaths = new Uint32Array( count );
|
|
409
454
|
this.specFieldSets = new Uint32Array( count );
|
|
410
455
|
this.specTypes = new Uint32Array( count );
|
|
@@ -591,6 +636,7 @@ class CrateReader {
|
|
|
591
636
|
readVector( item ) {
|
|
592
637
|
|
|
593
638
|
const n = this.u64();
|
|
639
|
+
this.records( n, 1 ); // every item takes at least a byte
|
|
594
640
|
const out = new Array( n );
|
|
595
641
|
for ( let i = 0; i < n; i ++ ) out[ i ] = item();
|
|
596
642
|
return out;
|
|
@@ -731,8 +777,8 @@ class CrateReader {
|
|
|
731
777
|
|
|
732
778
|
if ( compressed && pod[ 1 ] === 1 && n >= MIN_COMPRESSED_ARRAY_SIZE && ( kind === 'i32' || kind === 'u32' || kind === 'i64' || kind === 'u64' ) && this.version >= V0_5_0 ) {
|
|
733
779
|
|
|
734
|
-
const
|
|
735
|
-
|
|
780
|
+
const data = this.take( this.u64() );
|
|
781
|
+
this.decodedInts( n, kind === 'i32' || kind === 'u32' ? 4 : 8, data );
|
|
736
782
|
if ( kind === 'i32' ) out = decompressInts32( data, n );
|
|
737
783
|
else if ( kind === 'u32' ) out = new Uint32Array( decompressInts32( data, n ).buffer );
|
|
738
784
|
else out = decompressInts64( data, n, kind === 'u64' );
|
|
@@ -769,6 +815,7 @@ class CrateReader {
|
|
|
769
815
|
|
|
770
816
|
readArrayItems( n, item ) {
|
|
771
817
|
|
|
818
|
+
this.records( n, 4 );
|
|
772
819
|
const out = new Array( n );
|
|
773
820
|
for ( let i = 0; i < n; i ++ ) out[ i ] = item();
|
|
774
821
|
return out;
|
|
@@ -778,6 +825,7 @@ class CrateReader {
|
|
|
778
825
|
readRawArray( kind, count ) {
|
|
779
826
|
|
|
780
827
|
const size = SCALAR_SIZE[ kind ];
|
|
828
|
+
this.records( count, kind === 'f16' || kind === 'i64' || kind === 'u64' ? size * 3 : size ); // and any widened copy
|
|
781
829
|
const bytes = this.take( count * size ).slice(); // copy, also aligns
|
|
782
830
|
|
|
783
831
|
switch ( kind ) {
|
|
@@ -812,6 +860,8 @@ class CrateReader {
|
|
|
812
860
|
readCompressedFloats( kind, n ) {
|
|
813
861
|
|
|
814
862
|
const code = this.u8();
|
|
863
|
+
checkSize( n * 8, 'usdc' );
|
|
864
|
+
this.budget.decode( n * ( kind === 'f64' ? 8 : 4 ), 'usdc' );
|
|
815
865
|
const out = typedArrayFor( kind, n );
|
|
816
866
|
|
|
817
867
|
if ( code === 105 ) { // 'i': integers
|
|
@@ -912,11 +962,11 @@ function payloadListOp( value ) {
|
|
|
912
962
|
* @param {string} [identifier]
|
|
913
963
|
* @returns {Layer}
|
|
914
964
|
*/
|
|
915
|
-
export function readUSDC( data, identifier = 'anon.usdc' ) {
|
|
965
|
+
export function readUSDC( data, identifier = 'anon.usdc', { budget = new Budget() } = {} ) {
|
|
916
966
|
|
|
917
967
|
// a plain view, so subarray().slice() copies even for a Node Buffer
|
|
918
968
|
const bytes = ArrayBuffer.isView( data ) ? new Uint8Array( data.buffer, data.byteOffset, data.byteLength ) : new Uint8Array( data );
|
|
919
|
-
const crate = new CrateReader( bytes );
|
|
969
|
+
const crate = new CrateReader( bytes, budget );
|
|
920
970
|
const layer = new Layer( identifier );
|
|
921
971
|
|
|
922
972
|
const specs = new Map(); // path index -> { type, object, order }
|
package/src/usd/usdc/writer.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { AssetPath, DICT_TYPES, ListOp, Reference } from '../model.js';
|
|
5
5
|
import { compressFast } from './lz4.js';
|
|
6
6
|
import { compressInts32 } from './intcompression.js';
|
|
7
|
+
import { Budget } from '../../limits.js';
|
|
7
8
|
import { PERMISSIONS, POD, SPEC, SPECIFIERS, TYPE, crateTypeFor, floatToHalf, halfToFloat, isMatrixType, isVecType } from './types.js';
|
|
8
9
|
|
|
9
10
|
const IS_ARRAY = 0x80000000;
|
|
@@ -148,8 +149,9 @@ function pathElements( path ) {
|
|
|
148
149
|
|
|
149
150
|
class CrateWriter {
|
|
150
151
|
|
|
151
|
-
constructor() {
|
|
152
|
+
constructor( budget = new Budget() ) {
|
|
152
153
|
|
|
154
|
+
this.budget = budget;
|
|
153
155
|
this.buf = new Uint8Array( 1 << 16 );
|
|
154
156
|
this.view = new DataView( this.buf.buffer );
|
|
155
157
|
this.pos = 88; // bootstrap, written last
|
|
@@ -173,6 +175,7 @@ class CrateWriter {
|
|
|
173
175
|
this.fieldSetIndex = new Map();
|
|
174
176
|
this.specs = [];
|
|
175
177
|
this.timesCache = new Map();
|
|
178
|
+
this.arrayCache = new Map(); // hash -> [ { values, type, rep } ], so identical arrays are written once
|
|
176
179
|
|
|
177
180
|
}
|
|
178
181
|
|
|
@@ -181,8 +184,10 @@ class CrateWriter {
|
|
|
181
184
|
ensure( n ) {
|
|
182
185
|
|
|
183
186
|
if ( this.pos + n <= this.buf.length ) return;
|
|
187
|
+
this.budget.output( this.pos + n, 'usdc' );
|
|
184
188
|
let size = this.buf.length * 2;
|
|
185
189
|
while ( size < this.pos + n ) size *= 2;
|
|
190
|
+
size = Math.min( size, Math.max( this.pos + n, this.budget.maxOutputBytes ) );
|
|
186
191
|
const buf = new Uint8Array( size );
|
|
187
192
|
buf.set( this.buf );
|
|
188
193
|
this.buf = buf;
|
|
@@ -337,6 +342,7 @@ class CrateWriter {
|
|
|
337
342
|
|
|
338
343
|
addSpec( path, specType, fields ) {
|
|
339
344
|
|
|
345
|
+
this.budget.check();
|
|
340
346
|
const indexes = [];
|
|
341
347
|
|
|
342
348
|
for ( const [ name, [ lo, hi ] ] of fields ) {
|
|
@@ -690,8 +696,24 @@ class CrateWriter {
|
|
|
690
696
|
|
|
691
697
|
}
|
|
692
698
|
|
|
699
|
+
// Identical numeric arrays share one copy in the file, as OpenUSD's crate writer does.
|
|
693
700
|
packArray( values, type ) {
|
|
694
701
|
|
|
702
|
+
if ( ! POD[ type ] || values.length < MIN_COMPRESSED_ARRAY_SIZE ) return this.packArrayData( values, type );
|
|
703
|
+
|
|
704
|
+
const key = type + ':' + values.length + ':' + hashValues( values );
|
|
705
|
+
const entries = this.arrayCache.get( key ) || this.arrayCache.set( key, [] ).get( key );
|
|
706
|
+
const hit = entries.find( ( e ) => sameValues( e.values, values ) );
|
|
707
|
+
if ( hit ) return hit.rep;
|
|
708
|
+
|
|
709
|
+
const rep = this.packArrayData( values, type );
|
|
710
|
+
entries.push( { values, rep } );
|
|
711
|
+
return rep;
|
|
712
|
+
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
packArrayData( values, type ) {
|
|
716
|
+
|
|
695
717
|
const flags = ( IS_ARRAY | ( type << 16 ) ) >>> 0;
|
|
696
718
|
const pod = POD[ type ];
|
|
697
719
|
const total = values.length;
|
|
@@ -949,6 +971,33 @@ class CrateWriter {
|
|
|
949
971
|
|
|
950
972
|
}
|
|
951
973
|
|
|
974
|
+
const F64 = new Float64Array( 1 );
|
|
975
|
+
const F64_WORDS = new Int32Array( F64.buffer );
|
|
976
|
+
|
|
977
|
+
function hashValues( values ) {
|
|
978
|
+
|
|
979
|
+
let h = 0x811c9dc5;
|
|
980
|
+
|
|
981
|
+
for ( let i = 0; i < values.length; i ++ ) {
|
|
982
|
+
|
|
983
|
+
F64[ 0 ] = values[ i ];
|
|
984
|
+
h = Math.imul( h ^ F64_WORDS[ 0 ], 16777619 );
|
|
985
|
+
h = Math.imul( h ^ F64_WORDS[ 1 ], 16777619 );
|
|
986
|
+
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return h >>> 0;
|
|
990
|
+
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function sameValues( a, b ) {
|
|
994
|
+
|
|
995
|
+
if ( a.length !== b.length ) return false;
|
|
996
|
+
for ( let i = 0; i < a.length; i ++ ) if ( ! Object.is( a[ i ], b[ i ] ) ) return false;
|
|
997
|
+
return true;
|
|
998
|
+
|
|
999
|
+
}
|
|
1000
|
+
|
|
952
1001
|
function isInt8( v ) {
|
|
953
1002
|
|
|
954
1003
|
return Number.isInteger( v ) && v >= - 128 && v <= 127 && ! Object.is( v, - 0 );
|
|
@@ -1087,11 +1136,12 @@ function writeProperty( w, prop, path ) {
|
|
|
1087
1136
|
/**
|
|
1088
1137
|
* Writes a Layer as a binary USD crate file.
|
|
1089
1138
|
* @param {import('../model.js').Layer} layer
|
|
1139
|
+
* @param {{ budget?: import('../../limits.js').Budget }} [options]
|
|
1090
1140
|
* @returns {Uint8Array}
|
|
1091
1141
|
*/
|
|
1092
|
-
export function writeUSDC( layer ) {
|
|
1142
|
+
export function writeUSDC( layer, { budget = new Budget() } = {} ) {
|
|
1093
1143
|
|
|
1094
|
-
const w = new CrateWriter();
|
|
1144
|
+
const w = new CrateWriter( budget );
|
|
1095
1145
|
const root = layer.root;
|
|
1096
1146
|
|
|
1097
1147
|
const fields = metadataFields( w, layer.metadata );
|