@loaders.gl/scene 5.0.0-alpha.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.
Files changed (42) hide show
  1. package/LICENSE +41 -0
  2. package/README.md +23 -0
  3. package/dist/dist.dev.js +831 -0
  4. package/dist/dist.min.js +14 -0
  5. package/dist/index.cjs +65 -0
  6. package/dist/index.cjs.map +7 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +5 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/lib/parse-usd.d.ts +5 -0
  12. package/dist/lib/parse-usd.d.ts.map +1 -0
  13. package/dist/lib/parse-usd.js +276 -0
  14. package/dist/lib/parse-usd.js.map +1 -0
  15. package/dist/lib/parse-usda.d.ts +4 -0
  16. package/dist/lib/parse-usda.d.ts.map +1 -0
  17. package/dist/lib/parse-usda.js +395 -0
  18. package/dist/lib/parse-usda.js.map +1 -0
  19. package/dist/lib/parse-usdz.d.ts +3 -0
  20. package/dist/lib/parse-usdz.d.ts.map +1 -0
  21. package/dist/lib/parse-usdz.js +50 -0
  22. package/dist/lib/parse-usdz.js.map +1 -0
  23. package/dist/lib/usd-types.d.ts +86 -0
  24. package/dist/lib/usd-types.d.ts.map +1 -0
  25. package/dist/lib/usd-types.js +5 -0
  26. package/dist/lib/usd-types.js.map +1 -0
  27. package/dist/usd-loader-types.d.ts +50 -0
  28. package/dist/usd-loader-types.d.ts.map +1 -0
  29. package/dist/usd-loader-types.js +35 -0
  30. package/dist/usd-loader-types.js.map +1 -0
  31. package/dist/usd-loader.d.ts +30 -0
  32. package/dist/usd-loader.d.ts.map +1 -0
  33. package/dist/usd-loader.js +17 -0
  34. package/dist/usd-loader.js.map +1 -0
  35. package/package.json +55 -0
  36. package/src/index.ts +15 -0
  37. package/src/lib/parse-usd.ts +349 -0
  38. package/src/lib/parse-usda.ts +458 -0
  39. package/src/lib/parse-usdz.ts +59 -0
  40. package/src/lib/usd-types.ts +103 -0
  41. package/src/usd-loader-types.ts +40 -0
  42. package/src/usd-loader.ts +26 -0
@@ -0,0 +1,35 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+ // __VERSION__ is injected by babel-plugin-version-inline
5
+ // @ts-ignore TS2304: Cannot find name '__VERSION__'.
6
+ const VERSION = typeof "5.0.0-alpha.1" !== 'undefined' ? "5.0.0-alpha.1" : 'latest';
7
+ /** Loads the parser-bearing USD loader implementation. */
8
+ async function preload() {
9
+ const { USDLoaderWithParser } = await import('@loaders.gl/scene/usd-loader');
10
+ return USDLoaderWithParser;
11
+ }
12
+ /** Metadata-only loader for OpenUSD ASCII and uncompressed USDZ scenes. */
13
+ export const USDLoader = {
14
+ dataType: null,
15
+ batchType: null,
16
+ name: 'Universal Scene Description',
17
+ id: 'usd',
18
+ module: 'scene',
19
+ version: VERSION,
20
+ extensions: ['usd', 'usda', 'usdz'],
21
+ mimeTypes: ['model/vnd.usd', 'model/vnd.usda', 'model/vnd.usdz+zip'],
22
+ text: true,
23
+ binary: true,
24
+ tests: ['#usda', 'PK'],
25
+ options: {
26
+ usd: {
27
+ compose: true,
28
+ loadReferences: true,
29
+ maxReferenceDepth: 12,
30
+ variantSelections: {}
31
+ }
32
+ },
33
+ preload
34
+ };
35
+ //# sourceMappingURL=usd-loader-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usd-loader-types.js","sourceRoot":"","sources":["../src/usd-loader-types.ts"],"names":[],"mappings":"AAAA,aAAa;AACb,+BAA+B;AAC/B,oCAAoC;AAKpC,yDAAyD;AACzD,qDAAqD;AACrD,MAAM,OAAO,GAAG,sBAAkB,KAAK,WAAW,CAAC,CAAC,iBAAa,CAAC,CAAC,QAAQ,CAAC;AAE5E,0DAA0D;AAC1D,KAAK,UAAU,OAAO;IACpB,MAAM,EAAC,mBAAmB,EAAC,GAAG,MAAM,MAAM,CAAC,8BAA8B,CAAC,CAAC;IAC3E,OAAO,mBAAmB,CAAC;AAC7B,CAAC;AAED,2EAA2E;AAC3E,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,QAAQ,EAAE,IAA2B;IACrC,SAAS,EAAE,IAAa;IACxB,IAAI,EAAE,6BAA6B;IACnC,EAAE,EAAE,KAAK;IACT,MAAM,EAAE,OAAO;IACf,OAAO,EAAE,OAAO;IAChB,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC;IACnC,SAAS,EAAE,CAAC,eAAe,EAAE,gBAAgB,EAAE,oBAAoB,CAAC;IACpE,IAAI,EAAE,IAAI;IACV,MAAM,EAAE,IAAI;IACZ,KAAK,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC;IACtB,OAAO,EAAE;QACP,GAAG,EAAE;YACH,OAAO,EAAE,IAAI;YACb,cAAc,EAAE,IAAI;YACpB,iBAAiB,EAAE,EAAE;YACrB,iBAAiB,EAAE,EAAE;SACtB;KACF;IACD,OAAO;CACqD,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { parseUSD } from "./lib/parse-usd.js";
2
+ import type { USDLoaderOptions, USDStage } from "./lib/usd-types.js";
3
+ /** Parser-bearing loader for OpenUSD ASCII and uncompressed USDZ scenes. */
4
+ export declare const USDLoaderWithParser: {
5
+ readonly parse: typeof parseUSD;
6
+ readonly parseText: typeof parseText;
7
+ readonly dataType: USDStage;
8
+ readonly batchType: never;
9
+ readonly name: "Universal Scene Description";
10
+ readonly id: "usd";
11
+ readonly module: "scene";
12
+ readonly version: any;
13
+ readonly extensions: ["usd", "usda", "usdz"];
14
+ readonly mimeTypes: ["model/vnd.usd", "model/vnd.usda", "model/vnd.usdz+zip"];
15
+ readonly text: true;
16
+ readonly binary: true;
17
+ readonly tests: ["#usda", "PK"];
18
+ readonly options: {
19
+ readonly usd: {
20
+ readonly compose: true;
21
+ readonly loadReferences: true;
22
+ readonly maxReferenceDepth: 12;
23
+ readonly variantSelections: {};
24
+ };
25
+ };
26
+ };
27
+ /** Encodes USDA source text before using the shared binary parser. */
28
+ declare function parseText(text: string, options?: USDLoaderOptions, context?: Parameters<typeof parseUSD>[2]): Promise<USDStage>;
29
+ export {};
30
+ //# sourceMappingURL=usd-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usd-loader.d.ts","sourceRoot":"","sources":["../src/usd-loader.ts"],"names":[],"mappings":"AAKA,OAAO,EAAC,QAAQ,EAAC,2BAAwB;AACzC,OAAO,KAAK,EAAC,gBAAgB,EAAE,QAAQ,EAAC,2BAAwB;AAKhE,4EAA4E;AAC5E,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;CAIwC,CAAC;AAEzE,sEAAsE;AACtE,iBAAe,SAAS,CACtB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,gBAAgB,EAC1B,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,GACvC,OAAO,CAAC,QAAQ,CAAC,CAEnB"}
@@ -0,0 +1,17 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+ import { parseUSD } from "./lib/parse-usd.js";
5
+ import { USDLoader as USDLoaderMetadata } from "./usd-loader-types.js";
6
+ const { preload: _USDLoaderPreload, ...USDLoaderMetadataWithoutPreload } = USDLoaderMetadata;
7
+ /** Parser-bearing loader for OpenUSD ASCII and uncompressed USDZ scenes. */
8
+ export const USDLoaderWithParser = {
9
+ ...USDLoaderMetadataWithoutPreload,
10
+ parse: parseUSD,
11
+ parseText
12
+ };
13
+ /** Encodes USDA source text before using the shared binary parser. */
14
+ async function parseText(text, options, context) {
15
+ return parseUSD(new TextEncoder().encode(text).buffer, options, context);
16
+ }
17
+ //# sourceMappingURL=usd-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usd-loader.js","sourceRoot":"","sources":["../src/usd-loader.ts"],"names":[],"mappings":"AAAA,aAAa;AACb,+BAA+B;AAC/B,oCAAoC;AAGpC,OAAO,EAAC,QAAQ,EAAC,2BAAwB;AAEzC,OAAO,EAAC,SAAS,IAAI,iBAAiB,EAAC,8BAA2B;AAElE,MAAM,EAAC,OAAO,EAAE,iBAAiB,EAAE,GAAG,+BAA+B,EAAC,GAAG,iBAAiB,CAAC;AAE3F,4EAA4E;AAC5E,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,GAAG,+BAA+B;IAClC,KAAK,EAAE,QAAQ;IACf,SAAS;CAC6D,CAAC;AAEzE,sEAAsE;AACtE,KAAK,UAAU,SAAS,CACtB,IAAY,EACZ,OAA0B,EAC1B,OAAwC;IAExC,OAAO,QAAQ,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAC3E,CAAC"}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@loaders.gl/scene",
3
+ "version": "5.0.0-alpha.1",
4
+ "description": "Framework-independent loaders for scene description formats",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/visgl/loaders.gl"
13
+ },
14
+ "keywords": [
15
+ "loader",
16
+ "3d",
17
+ "scene",
18
+ "OpenUSD",
19
+ "USD",
20
+ "USDA",
21
+ "USDZ"
22
+ ],
23
+ "types": "dist/index.d.ts",
24
+ "main": "dist/index.cjs",
25
+ "module": "dist/index.js",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js",
30
+ "require": "./dist/index.cjs"
31
+ },
32
+ "./usd-loader": {
33
+ "types": "./dist/usd-loader.d.ts",
34
+ "import": "./dist/usd-loader.js"
35
+ }
36
+ },
37
+ "sideEffects": false,
38
+ "files": [
39
+ "src",
40
+ "dist",
41
+ "README.md"
42
+ ],
43
+ "scripts": {
44
+ "pre-build": "npm run build-bundle && npm run build-bundle-dev",
45
+ "build-bundle": "ocular-bundle ./bundle.ts --output=dist/dist.min.js",
46
+ "build-bundle-dev": "ocular-bundle ./bundle.ts --env=dev --output=dist/dist.dev.js"
47
+ },
48
+ "dependencies": {
49
+ "@loaders.gl/loader-utils": "5.0.0-alpha.1"
50
+ },
51
+ "peerDependencies": {
52
+ "@loaders.gl/core": "~5.0.0-alpha.0"
53
+ },
54
+ "gitHead": "77a55028e8204e09df1258f10cb61ed35353b258"
55
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ export {USDLoader} from './usd-loader-types';
6
+ export type {
7
+ USDAssetPath,
8
+ USDAttribute,
9
+ USDLoaderOptions,
10
+ USDPrim,
11
+ USDScenePath,
12
+ USDStage,
13
+ USDValue,
14
+ USDVariant
15
+ } from './lib/usd-types';
@@ -0,0 +1,349 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {LoaderContext} from '@loaders.gl/loader-utils';
6
+ import {parseUSDA} from './parse-usda';
7
+ import {parseUSDZArchive} from './parse-usdz';
8
+ import type {USDAssetPath, USDLoaderOptions, USDPrim, USDStage, USDValue} from './usd-types';
9
+
10
+ type USDParseEnvironment = {
11
+ /** Fetch implementation used for external layers. */
12
+ fetch: (url: string) => Promise<Response>;
13
+ /** Pending and resolved external layers keyed by URL. */
14
+ cache: Map<string, Promise<USDStage>>;
15
+ /** Files extracted from the active USDZ archive. */
16
+ archiveFiles: Map<string, ArrayBuffer>;
17
+ /** Layer URLs participating in the composed stage. */
18
+ layers: Set<string>;
19
+ /** Effective loader options. */
20
+ options: USDLoaderOptions;
21
+ };
22
+
23
+ const TEXT_DECODER = new TextDecoder();
24
+ const CRATE_SIGNATURE = 'PXR-USDC';
25
+ const ZIP_SIGNATURE = 0x04034b50;
26
+
27
+ /** Parses and optionally composes an OpenUSD stage. */
28
+ export async function parseUSD(
29
+ data: ArrayBuffer,
30
+ options: USDLoaderOptions = {},
31
+ context?: LoaderContext
32
+ ): Promise<USDStage> {
33
+ const url = getSourceLocation(options.core?.baseUrl, context?.url);
34
+ const environment: USDParseEnvironment = {
35
+ fetch: async resourceUrl => {
36
+ const response = await (context?.fetch || fetch)(resourceUrl);
37
+ if (!(response instanceof Response)) {
38
+ throw new Error(`OpenUSD reference "${resourceUrl}" did not return an HTTP response.`);
39
+ }
40
+ if (!response.ok) {
41
+ throw new Error(`Unable to load OpenUSD reference "${resourceUrl}": ${response.status}.`);
42
+ }
43
+ return response;
44
+ },
45
+ cache: new Map(),
46
+ archiveFiles: new Map(),
47
+ layers: new Set(),
48
+ options: {
49
+ ...options,
50
+ usd: {...USD_LOADER_DEFAULT_OPTIONS, ...options.usd}
51
+ }
52
+ };
53
+
54
+ let stage: USDStage;
55
+ if (isZIPArchive(data)) {
56
+ const archive = parseUSDZArchive(data);
57
+ const rootFilename = Array.from(archive.keys()).find(filename => /\.usda?$/i.test(filename));
58
+ if (!rootFilename) {
59
+ throw new Error('USDZ archives with binary USDC root layers are not implemented yet.');
60
+ }
61
+
62
+ const archiveBase = 'https://usd.archive/';
63
+ for (const [filename, content] of archive) {
64
+ environment.archiveFiles.set(new URL(filename, archiveBase).href, content);
65
+ }
66
+ const rootUrl = new URL(rootFilename, archiveBase).href;
67
+ stage = parseUSDLayer(archive.get(rootFilename)!, rootUrl);
68
+ stage.format = 'usdz';
69
+ } else {
70
+ stage = parseUSDLayer(data, url);
71
+ }
72
+
73
+ if (stage.url) {
74
+ environment.layers.add(stage.url);
75
+ }
76
+ if (environment.options.usd?.compose !== false) {
77
+ stage.rootPrims = await composePrims(stage.rootPrims, stage.url, {}, environment, 0);
78
+ }
79
+ stage.layers = Array.from(environment.layers);
80
+ if (url) {
81
+ stage.url = url;
82
+ }
83
+ return stage;
84
+ }
85
+
86
+ /** Selects a source location suitable for resolving referenced layers. */
87
+ function getSourceLocation(
88
+ configuredLocation: string | undefined,
89
+ contextLocation: string | undefined
90
+ ): string | undefined {
91
+ const configuredUrl = getAbsoluteUrl(configuredLocation);
92
+ if (configuredUrl) {
93
+ return configuredUrl;
94
+ }
95
+ const contextUrl = getAbsoluteUrl(contextLocation);
96
+ return contextUrl || configuredLocation || contextLocation;
97
+ }
98
+
99
+ /** Returns an absolute URL when the candidate contains one. */
100
+ function getAbsoluteUrl(candidate: string | undefined): string | undefined {
101
+ try {
102
+ return candidate ? new URL(candidate).href : undefined;
103
+ } catch {
104
+ return undefined;
105
+ }
106
+ }
107
+
108
+ /** Resolves an asset path against either a URL or a filesystem path. */
109
+ function resolveAssetPath(assetPath: string, sourceLocation: string): string {
110
+ try {
111
+ return new URL(assetPath, sourceLocation).href;
112
+ } catch {
113
+ const slashIndex = Math.max(sourceLocation.lastIndexOf('/'), sourceLocation.lastIndexOf('\\'));
114
+ return slashIndex >= 0 ? `${sourceLocation.slice(0, slashIndex + 1)}${assetPath}` : assetPath;
115
+ }
116
+ }
117
+
118
+ const USD_LOADER_DEFAULT_OPTIONS = {
119
+ compose: true,
120
+ loadReferences: true,
121
+ maxReferenceDepth: 12,
122
+ variantSelections: {}
123
+ };
124
+
125
+ /** Parses one ASCII or binary OpenUSD layer. */
126
+ function parseUSDLayer(data: ArrayBuffer, url?: string): USDStage {
127
+ const signature = TEXT_DECODER.decode(data.slice(0, 8));
128
+ if (signature === CRATE_SIGNATURE) {
129
+ throw new Error('Binary USDC crate layers are not implemented yet; use ASCII USDA layers.');
130
+ }
131
+ return parseUSDA(TEXT_DECODER.decode(data), url);
132
+ }
133
+
134
+ /** Recursively applies variants, references, overrides, and class filtering. */
135
+ async function composePrims(
136
+ prims: USDPrim[],
137
+ sourceUrl: string | undefined,
138
+ inheritedVariants: Record<string, string>,
139
+ environment: USDParseEnvironment,
140
+ depth: number
141
+ ): Promise<USDPrim[]> {
142
+ if (depth > (environment.options.usd?.maxReferenceDepth ?? 12)) {
143
+ throw new Error('OpenUSD reference composition exceeded the configured depth limit.');
144
+ }
145
+
146
+ const composedPrims: USDPrim[] = [];
147
+ for (const sourcePrim of prims) {
148
+ if (sourcePrim.specifier === 'class') {
149
+ continue;
150
+ }
151
+
152
+ const localSelections = getVariantSelections(sourcePrim.metadata['variants']);
153
+ const selections = {
154
+ ...localSelections,
155
+ ...inheritedVariants,
156
+ ...environment.options.usd?.variantSelections
157
+ };
158
+ const primSourceUrl = sourcePrim.sourceUrl || sourceUrl;
159
+ let composedPrim = clonePrim(sourcePrim);
160
+
161
+ for (const [variantSetName, variantSet] of Object.entries(composedPrim.variants)) {
162
+ const selection = selections[variantSetName] || Object.keys(variantSet)[0];
163
+ const variant = variantSet[selection];
164
+ if (variant) {
165
+ composedPrim = mergePrim(composedPrim, {
166
+ ...composedPrim,
167
+ attributes: variant.attributes,
168
+ metadata: variant.metadata,
169
+ children: variant.children,
170
+ variants: {}
171
+ });
172
+ }
173
+ }
174
+
175
+ const references = getPrimReferences(composedPrim);
176
+ if (environment.options.usd?.loadReferences !== false && references.length > 0) {
177
+ for (const reference of references) {
178
+ if (!primSourceUrl) {
179
+ throw new Error('OpenUSD references require a source URL or options.core.baseUrl.');
180
+ }
181
+ const referencedUrl = resolveAssetPath(reference.assetPath, primSourceUrl);
182
+ const stage = await loadReferencedStage(referencedUrl, environment);
183
+ const referencedPrims = selectReferencedPrims(stage, reference.primPath);
184
+ const resolvedPrims = await composePrims(
185
+ referencedPrims,
186
+ referencedUrl,
187
+ selections,
188
+ environment,
189
+ depth + 1
190
+ );
191
+ for (const referencedPrim of resolvedPrims) {
192
+ composedPrim = mergePrim(rebasePrim(referencedPrim, composedPrim.path), composedPrim);
193
+ }
194
+ }
195
+ }
196
+
197
+ composedPrim.children = await composePrims(
198
+ composedPrim.children,
199
+ primSourceUrl,
200
+ selections,
201
+ environment,
202
+ depth
203
+ );
204
+ composedPrims.push(composedPrim);
205
+ }
206
+
207
+ return composedPrims;
208
+ }
209
+
210
+ /** Loads and parses one referenced layer, with per-parse caching. */
211
+ async function loadReferencedStage(
212
+ url: string,
213
+ environment: USDParseEnvironment
214
+ ): Promise<USDStage> {
215
+ let pendingStage = environment.cache.get(url);
216
+ if (!pendingStage) {
217
+ pendingStage = (async () => {
218
+ const archiveContent = environment.archiveFiles.get(url);
219
+ let content: ArrayBuffer | Promise<ArrayBuffer> | undefined = archiveContent;
220
+ if (!content) {
221
+ let response: Response;
222
+ try {
223
+ response = await environment.fetch(url);
224
+ } catch (error) {
225
+ throw new Error(`Unable to fetch USD layer "${url}": ${String(error)}`);
226
+ }
227
+ content = response.arrayBuffer();
228
+ }
229
+ const stage = parseUSDLayer(await content, url);
230
+ environment.layers.add(url);
231
+ return stage;
232
+ })();
233
+ environment.cache.set(url, pendingStage);
234
+ }
235
+ return pendingStage;
236
+ }
237
+
238
+ /** Selects the referenced prim or default prim from a stage. */
239
+ function selectReferencedPrims(stage: USDStage, primPath?: string): USDPrim[] {
240
+ const targetPath = primPath || stage.metadata['defaultPrim'];
241
+ if (typeof targetPath !== 'string') {
242
+ return stage.rootPrims;
243
+ }
244
+
245
+ const normalizedPath = targetPath.startsWith('/') ? targetPath : `/${targetPath}`;
246
+ const target = findPrim(stage.rootPrims, normalizedPath);
247
+ return target ? [target] : [];
248
+ }
249
+
250
+ /** Finds a prim recursively by absolute path. */
251
+ function findPrim(prims: USDPrim[], path: string): USDPrim | undefined {
252
+ for (const prim of prims) {
253
+ if (prim.path === path) {
254
+ return prim;
255
+ }
256
+ const child = findPrim(prim.children, path);
257
+ if (child) {
258
+ return child;
259
+ }
260
+ }
261
+ return undefined;
262
+ }
263
+
264
+ /** Collects reference and payload asset paths from a prim. */
265
+ function getPrimReferences(prim: USDPrim): USDAssetPath[] {
266
+ const references: USDAssetPath[] = [];
267
+ for (const value of [
268
+ prim.metadata['references'],
269
+ prim.metadata['payload'],
270
+ prim.metadata['payloads']
271
+ ]) {
272
+ if (Array.isArray(value)) {
273
+ references.push(...value.filter(isAssetPath));
274
+ } else if (isAssetPath(value)) {
275
+ references.push(value);
276
+ }
277
+ }
278
+ return references;
279
+ }
280
+
281
+ /** Converts authored variant metadata into a selection map. */
282
+ function getVariantSelections(value: USDValue | undefined): Record<string, string> {
283
+ if (!value || typeof value !== 'object' || Array.isArray(value) || isAssetPath(value)) {
284
+ return {};
285
+ }
286
+ return Object.fromEntries(
287
+ Object.entries(value).filter((entry): entry is [string, string] => typeof entry[1] === 'string')
288
+ );
289
+ }
290
+
291
+ /** Returns whether a parsed value is an external asset path. */
292
+ function isAssetPath(value: USDValue | undefined): value is USDAssetPath {
293
+ return Boolean(
294
+ value && typeof value === 'object' && !Array.isArray(value) && 'assetPath' in value
295
+ );
296
+ }
297
+
298
+ /** Deep-clones a prim hierarchy while preserving scalar values. */
299
+ function clonePrim(prim: USDPrim): USDPrim {
300
+ return {
301
+ ...prim,
302
+ attributes: {...prim.attributes},
303
+ metadata: {...prim.metadata},
304
+ variants: {...prim.variants},
305
+ children: prim.children.map(clonePrim)
306
+ };
307
+ }
308
+
309
+ /** Rebases a prim hierarchy at a new composed path. */
310
+ function rebasePrim(prim: USDPrim, path: string): USDPrim {
311
+ const rebased = clonePrim(prim);
312
+ rebased.path = path;
313
+ rebased.children = rebased.children.map(child => rebasePrim(child, `${path}/${child.name}`));
314
+ return rebased;
315
+ }
316
+
317
+ /** Merges a stronger local prim opinion over a referenced prim. */
318
+ function mergePrim(reference: USDPrim, override: USDPrim): USDPrim {
319
+ const children = reference.children.map(clonePrim);
320
+ for (const overridingChild of override.children) {
321
+ const matchingIndex = children.findIndex(child => child.name === overridingChild.name);
322
+ if (matchingIndex >= 0) {
323
+ children[matchingIndex] = mergePrim(children[matchingIndex], overridingChild);
324
+ } else {
325
+ children.push(clonePrim(overridingChild));
326
+ }
327
+ }
328
+
329
+ return {
330
+ ...reference,
331
+ ...override,
332
+ sourceUrl:
333
+ getPrimReferences(override).length > 0
334
+ ? override.sourceUrl
335
+ : reference.sourceUrl || override.sourceUrl,
336
+ type: override.type || reference.type,
337
+ attributes: {...reference.attributes, ...override.attributes},
338
+ metadata: {...reference.metadata, ...override.metadata},
339
+ variants: {...reference.variants, ...override.variants},
340
+ children
341
+ };
342
+ }
343
+
344
+ /** Returns whether a buffer starts with a ZIP local-file signature. */
345
+ function isZIPArchive(arrayBuffer: ArrayBuffer): boolean {
346
+ return (
347
+ arrayBuffer.byteLength >= 4 && new DataView(arrayBuffer).getUint32(0, true) === ZIP_SIGNATURE
348
+ );
349
+ }