three-usd-robot 0.6.0 → 0.7.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 CHANGED
@@ -9,6 +9,11 @@ Think of it as a **USD version of [`urdf-loader`](https://www.npmjs.com/package/
9
9
  it reads the link / joint / xform / mesh structure out of `UsdPhysics` assets
10
10
  and drives forward kinematics on a Three.js `Object3D` hierarchy.
11
11
 
12
+ **[▶ Live demo](https://three-usd-robot.vercel.app)** — stock Isaac Sim robots
13
+ streamed from NVIDIA's asset CDN, joints driven from a slider panel, and
14
+ exported back to `.usda` / `.usdz` in the browser.
15
+
16
+ ![A franka panda loaded in the browser](assets/franka.png)
12
17
  ![A robot cell loaded in the browser](assets/threejs.png)
13
18
 
14
19
  ## Features
@@ -25,8 +30,26 @@ and drives forward kinematics on a Three.js `Object3D` hierarchy.
25
30
  simulation-ready for Isaac Sim.
26
31
  - **React** — declarative `<UsdRobot>` for React Three Fiber.
27
32
 
28
- Not yet supported: time samples and variant *selections* stored inside binary
29
- crate files, and full material/shader fidelity.
33
+ Stock Isaac Sim robot assets load straight from their public CDN — Franka
34
+ Panda, UR10e, Fanuc CRX-10iA/L, Kuka KR210, Shadow Hand, Unitree H1/Go2 and
35
+ friends all compose from their variant-driven, multi-layer form:
36
+
37
+ ```ts
38
+ const ROOT = "https://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/5.1";
39
+ const franka = await new ThreeUsdRobotLoader()
40
+ .loadAsync(`${ROOT}/Isaac/Robots/FrankaRobotics/FrankaPanda/franka.usd`);
41
+
42
+ franka.setJointValues({ panda_joint2: -0.785, panda_joint4: -2.356, panda_joint6: 1.571 });
43
+ franka.getLinkWorldPosition("panda_hand"); // (0.307, 0, 0.590) — the documented ready pose
44
+ ```
45
+
46
+ The CDN is public and CORS-enabled, so this works in the browser too. Try
47
+ `npx tsx scripts/demo-franka.ts` for a console walkthrough (articulation table,
48
+ FK check, and a re-export to one self-contained file), or open the Vite example
49
+ and pick a robot from the preset list.
50
+
51
+ Not yet supported: time samples stored inside binary crate files, non-mesh
52
+ gprims (`Cube`, `Sphere`, …), and full material/shader fidelity.
30
53
 
31
54
  ## Install
32
55
 
@@ -198,9 +221,16 @@ and `.usdz` packages to ASCII USDA.
198
221
 
199
222
  ## Examples
200
223
 
201
- [`examples/`](./examples) holds runnable Vite demos, including a React Three
202
- Fiber viewer that loads any asset via `?asset=<url>` and exports it back to
203
- `.usda` / `.usdz`.
224
+ [`examples/`](./examples) holds runnable Vite demos:
225
+
226
+ - **`vite-joint-slider`** — the [live demo](https://three-usd-robot.vercel.app):
227
+ vanilla Three.js + `lil-gui`, with a robot picker, joint sliders, animation
228
+ playback and USD export.
229
+ - **`vite-basic-viewer`** — the same thing through React Three Fiber.
230
+
231
+ Both take `?asset=<url>` for any asset, or `?isaac=<path under Isaac/>` to pull
232
+ one straight from NVIDIA's CDN. `npm run demo:build` generates the factory cell
233
+ and builds the deployable site.
204
234
 
205
235
  ## Package entry points
206
236
 
@@ -1,5 +1,5 @@
1
1
  import * as THREE from 'three';
2
- import { J as JointType, A as Axis, a as JointDescription, L as LinkDescription, R as RobotDescription, K as KinematicTree } from './buildKinematicTree-wtZPzy0B.js';
2
+ import { J as JointType, A as Axis, a as JointDescription, L as LinkDescription, R as RobotDescription, K as KinematicTree } from './buildKinematicTree-Ccd2DIKH.js';
3
3
 
4
4
  /**
5
5
  * The articulated "motion" node of a joint, inserted between the joint's two
@@ -1,6 +1,6 @@
1
- import { R as RobotDescription } from './buildKinematicTree-wtZPzy0B.js';
1
+ import { R as RobotDescription } from './buildKinematicTree-Ccd2DIKH.js';
2
2
  import { A as AssetResolver } from './AssetResolver-CpIJNgWZ.js';
3
- import { T as ThreeUsdRobot } from './ThreeUsdRobot-CajXJqYU.js';
3
+ import { T as ThreeUsdRobot } from './ThreeUsdRobot-Ljh4PDVD.js';
4
4
 
5
5
  type ThreeUsdRobotLoaderOptions = {
6
6
  /** Resolver for references / payloads / sublayers (default {@link DefaultAssetResolver}). */
@@ -89,6 +89,11 @@ type PrimSpec = {
89
89
  type VariantContent = {
90
90
  properties: PropertySpec[];
91
91
  children: PrimSpec[];
92
+ /**
93
+ * Metadata authored on the variant itself. Composition arcs live here — the
94
+ * common "this variant references another layer" pattern in Isaac Sim assets.
95
+ */
96
+ metadata: MetadataMap;
92
97
  };
93
98
  /** `variantSetName → variantName → content`. */
94
99
  type VariantSetMap = {
@@ -394,6 +394,24 @@ function isNonVisualPurpose(prim) {
394
394
  const p = getPurpose(prim);
395
395
  return p === "guide" || p === "proxy";
396
396
  }
397
+ function getMaterialSubsets(meshPrim) {
398
+ const out = [];
399
+ for (const child of meshPrim.GetChildren()) {
400
+ if (child.GetTypeName() !== "GeomSubset") continue;
401
+ const elementType = child.GetAttribute("elementType").Get();
402
+ if (elementType !== void 0 && elementType !== "face") continue;
403
+ const family = child.GetAttribute("familyName").Get();
404
+ if (family !== void 0 && family !== "materialBind") continue;
405
+ const indices = child.GetAttribute("indices").Get();
406
+ if (!Array.isArray(indices)) continue;
407
+ const faces = [];
408
+ for (const i of indices) {
409
+ if (typeof i === "number") faces.push(i);
410
+ }
411
+ if (faces.length > 0) out.push({ prim: child, faces });
412
+ }
413
+ return out;
414
+ }
397
415
  function* iterDescendants(prim) {
398
416
  for (const child of prim.GetChildren()) {
399
417
  yield child;
@@ -549,9 +567,7 @@ function computeLocalTransform(prim) {
549
567
  opName = opName.slice(INVERT_PREFIX.length);
550
568
  }
551
569
  const attr = prim.GetAttribute(opName);
552
- if (!attr.IsValid()) {
553
- throw new Error(`${prim.GetPath()}: xformOpOrder references missing op "${opName}"`);
554
- }
570
+ if (!attr.IsValid()) continue;
555
571
  const opValue = attr.Get();
556
572
  if (opValue === void 0) continue;
557
573
  let opMatrix = opMatrixFor(parseOpType(opName), opValue, `${prim.GetPath()}.${opName}`);
@@ -660,7 +676,7 @@ function resolveBoundMaterial(stage, prim) {
660
676
  if (!material) return void 0;
661
677
  const shader = findSurfaceShader(material);
662
678
  if (!shader) return void 0;
663
- const result = {};
679
+ const result = { name: material.GetName() };
664
680
  const color = firstColor(shader, DIFFUSE_INPUTS);
665
681
  if (color) result.color = color;
666
682
  const opacity = firstNumber(shader, OPACITY_INPUTS);
@@ -994,8 +1010,10 @@ function buildLink(path, prim) {
994
1010
  const collisionPrims = [];
995
1011
  for (const meshPath of gatherMeshDescendants(prim)) {
996
1012
  const mp = prim.GetStage().GetPrimAtPath(meshPath);
997
- if (mp && (hasCollisionAPI(mp) || isNonVisualPurpose(mp))) collisionPrims.push(meshPath);
998
- else visualPrims.push(meshPath);
1013
+ if (!mp) continue;
1014
+ const nonVisual = isNonVisualPurpose(mp);
1015
+ if (!nonVisual) visualPrims.push(meshPath);
1016
+ if (nonVisual || hasCollisionAPI(mp)) collisionPrims.push(meshPath);
999
1017
  }
1000
1018
  const inertial = getMassProperties(prim);
1001
1019
  const worldTransform = computeWorldTransform(prim);
@@ -1631,11 +1649,11 @@ function parseVariantSet(r) {
1631
1649
  const variants = {};
1632
1650
  while (!r.is("rbrace") && !r.atEnd()) {
1633
1651
  const variantName = r.expect("string").value;
1634
- if (r.is("lparen")) parseMetadataBlock(r);
1652
+ const metadata = r.is("lparen") ? parseMetadataBlock(r) : {};
1635
1653
  r.expect("lbrace");
1636
1654
  const body = parseBody(r);
1637
1655
  r.expect("rbrace");
1638
- variants[variantName] = { properties: body.properties, children: body.children };
1656
+ variants[variantName] = { properties: body.properties, children: body.children, metadata };
1639
1657
  }
1640
1658
  r.expect("rbrace");
1641
1659
  return { setName, variants };
@@ -1845,7 +1863,13 @@ function pushPrimBody(out, properties, children, variantSets, level) {
1845
1863
  for (const [setName, variants] of Object.entries(variantSets)) {
1846
1864
  out.push(`${pad}variantSet ${quoteString(setName)} = {`);
1847
1865
  for (const [variantName, content] of Object.entries(variants)) {
1848
- out.push(`${pad}${INDENT}${quoteString(variantName)} {`);
1866
+ const head = `${pad}${INDENT}${quoteString(variantName)}`;
1867
+ if (Object.keys(content.metadata).length > 0) {
1868
+ pushWithMetadata(out, head, content.metadata, level + 1);
1869
+ out.push(`${pad}${INDENT}{`);
1870
+ } else {
1871
+ out.push(`${head} {`);
1872
+ }
1849
1873
  pushPrimBody(out, content.properties, content.children, void 0, level + 2);
1850
1874
  out.push(`${pad}${INDENT}}`);
1851
1875
  }
@@ -2510,6 +2534,8 @@ var CrateType = {
2510
2534
  Specifier: 42,
2511
2535
  Permission: 43,
2512
2536
  Variability: 44,
2537
+ VariantSelectionMap: 45,
2538
+ StringVector: 50,
2513
2539
  PayloadListOp: 55};
2514
2540
  var ListOpBits = {
2515
2541
  HasExplicit: 1 << 1,
@@ -2865,6 +2891,10 @@ var CrateReader = class {
2865
2891
  return this.readIndexVector(off, "token").items;
2866
2892
  case CrateType.PathVector:
2867
2893
  return this.readIndexVector(off, "path").items;
2894
+ case CrateType.StringVector:
2895
+ return this.readIndexVector(off, "string").items;
2896
+ case CrateType.VariantSelectionMap:
2897
+ return this.readVariantSelectionMap(off);
2868
2898
  default:
2869
2899
  return void 0;
2870
2900
  }
@@ -2931,6 +2961,24 @@ var CrateReader = class {
2931
2961
  }
2932
2962
  return out;
2933
2963
  }
2964
+ /**
2965
+ * Read an `SdfVariantSelectionMap`: `[u64 count]` then `count` pairs of
2966
+ * string indexes (`variantSetName → variantName`). Surfaces as a dictionary
2967
+ * so it maps straight onto the USDA `variants = { ... }` metadatum.
2968
+ */
2969
+ readVariantSelectionMap(off) {
2970
+ const count = this.u64(off);
2971
+ const strings = this.getStrings();
2972
+ const out = {};
2973
+ let p = off + 8;
2974
+ for (let i = 0; i < count; i++) {
2975
+ const key = this.getToken(strings[this.view.getUint32(p, true)] ?? 0);
2976
+ const value = this.getToken(strings[this.view.getUint32(p + 4, true)] ?? 0);
2977
+ if (key) out[key] = value;
2978
+ p += 8;
2979
+ }
2980
+ return out;
2981
+ }
2934
2982
  /** Read a `[u64 count][count × uint32|int32 index]` vector → resolved strings. */
2935
2983
  readIndexVector(off, kind) {
2936
2984
  const count = this.u64(off);
@@ -2938,7 +2986,7 @@ var CrateReader = class {
2938
2986
  const items = new Array(count);
2939
2987
  for (let i = 0; i < count; i++) {
2940
2988
  const idx = this.view.getInt32(p, true);
2941
- items[i] = kind === "token" ? this.getToken(idx) : this.getPaths()[idx] ?? "";
2989
+ items[i] = kind === "token" ? this.getToken(idx) : kind === "string" ? this.getToken(this.getStrings()[idx] ?? 0) : this.getPaths()[idx] ?? "";
2942
2990
  p += 4;
2943
2991
  }
2944
2992
  return { items, next: p };
@@ -3020,6 +3068,7 @@ var SPEC_PRIM = 6;
3020
3068
  var SPEC_PSEUDO_ROOT = 7;
3021
3069
  var SPEC_ATTRIBUTE = 1;
3022
3070
  var SPEC_RELATIONSHIP = 8;
3071
+ var SPEC_VARIANT = 10;
3023
3072
  var SPECIFIERS2 = ["def", "over", "class"];
3024
3073
  function crateToUsdaFile(crate) {
3025
3074
  const paths = crate.getPaths();
@@ -3034,8 +3083,26 @@ function crateToUsdaFile(crate) {
3034
3083
  return map;
3035
3084
  };
3036
3085
  const primByPath = /* @__PURE__ */ new Map();
3086
+ const variantByPath = /* @__PURE__ */ new Map();
3037
3087
  const rootPrims = [];
3038
3088
  let layerMetadata = {};
3089
+ const containerAt = (path) => {
3090
+ const prim = primByPath.get(path);
3091
+ if (prim) return prim;
3092
+ const cached = variantByPath.get(path);
3093
+ if (cached) return cached;
3094
+ const selection = variantNode(path);
3095
+ if (!selection) return void 0;
3096
+ const owner = primByPath.get(selection.owner);
3097
+ if (!owner) return void 0;
3098
+ owner.variantSets ??= {};
3099
+ owner.variantSets[selection.setName] ??= {};
3100
+ const set = owner.variantSets[selection.setName];
3101
+ set[selection.variantName] ??= { properties: [], children: [], metadata: {} };
3102
+ const content = set[selection.variantName];
3103
+ variantByPath.set(path, content);
3104
+ return content;
3105
+ };
3039
3106
  for (const spec of specs) {
3040
3107
  const path = paths[spec.pathIndex] ?? "";
3041
3108
  if (spec.specType === SPEC_PSEUDO_ROOT) {
@@ -3054,24 +3121,41 @@ function crateToUsdaFile(crate) {
3054
3121
  line: 0
3055
3122
  });
3056
3123
  }
3124
+ for (const spec of specs) {
3125
+ if (spec.specType !== SPEC_VARIANT) continue;
3126
+ const content = containerAt(paths[spec.pathIndex] ?? "");
3127
+ if (content)
3128
+ Object.assign(content.metadata, buildPrimMetadata(crate, fieldsOf(spec.fieldSetIndex)));
3129
+ }
3057
3130
  for (const spec of specs) {
3058
3131
  if (spec.specType !== SPEC_ATTRIBUTE && spec.specType !== SPEC_RELATIONSHIP) continue;
3059
3132
  const split = splitProperty(paths[spec.pathIndex] ?? "");
3060
3133
  if (!split) continue;
3061
- const prim = primByPath.get(split.primPath);
3062
- if (!prim) continue;
3134
+ const owner = containerAt(split.primPath);
3135
+ if (!owner) continue;
3063
3136
  const fm = fieldsOf(spec.fieldSetIndex);
3064
- prim.properties.push(
3137
+ owner.properties.push(
3065
3138
  spec.specType === SPEC_ATTRIBUTE ? buildAttribute(crate, split.propName, fm) : buildRelationship(crate, split.propName, fm)
3066
3139
  );
3067
3140
  }
3068
3141
  for (const [path, prim] of primByPath) {
3069
3142
  const parentPath = parentOf(path);
3070
3143
  if (parentPath === "/") rootPrims.push(prim);
3071
- else primByPath.get(parentPath)?.children.push(prim);
3144
+ else containerAt(parentPath)?.children.push(prim);
3072
3145
  }
3073
3146
  return { version: crate.version.join("."), metadata: layerMetadata, prims: rootPrims };
3074
3147
  }
3148
+ function variantNode(path) {
3149
+ if (!path.endsWith("}")) return null;
3150
+ const open = path.lastIndexOf("{");
3151
+ if (open < 0) return null;
3152
+ const selection = path.slice(open + 1, -1);
3153
+ const eq = selection.indexOf("=");
3154
+ if (eq < 0) return null;
3155
+ const variantName = selection.slice(eq + 1);
3156
+ if (variantName === "") return null;
3157
+ return { owner: path.slice(0, open), setName: selection.slice(0, eq), variantName };
3158
+ }
3075
3159
  function buildAttribute(crate, name, fm) {
3076
3160
  const defaultRep = fm.get("default");
3077
3161
  const rawType = asString(crate, fm.get("typeName")) ?? "";
@@ -3112,6 +3196,10 @@ function buildPrimMetadata(crate, fm) {
3112
3196
  if (Array.isArray(apiSchemas)) meta.apiSchemas = apiSchemas;
3113
3197
  const kind = asString(crate, fm.get("kind"));
3114
3198
  if (kind !== void 0) meta.kind = kind;
3199
+ const variants = fm.has("variantSelection") ? crate.getValue(fm.get("variantSelection")) : void 0;
3200
+ if (variants && typeof variants === "object" && !Array.isArray(variants)) {
3201
+ meta.variants = variants;
3202
+ }
3115
3203
  for (const key of ARC_FIELDS) {
3116
3204
  if (!fm.has(key)) continue;
3117
3205
  const arcs = crate.getValue(fm.get(key));
@@ -3121,6 +3209,11 @@ function buildPrimMetadata(crate, fm) {
3121
3209
  }
3122
3210
  function buildLayerMetadata(crate, fm) {
3123
3211
  const meta = {};
3212
+ const subLayers = fm.has("subLayers") ? crate.getValue(fm.get("subLayers")) : void 0;
3213
+ if (Array.isArray(subLayers)) {
3214
+ const paths = subLayers.filter((s) => typeof s === "string" && s.length > 0);
3215
+ if (paths.length > 0) meta.subLayers = paths.map((p) => new AssetPath(p));
3216
+ }
3124
3217
  const upAxis = asString(crate, fm.get("upAxis"));
3125
3218
  if (upAxis !== void 0) meta.upAxis = upAxis;
3126
3219
  const defaultPrim2 = asString(crate, fm.get("defaultPrim"));
@@ -3157,16 +3250,24 @@ function leaf(path) {
3157
3250
  // src/usd/composition.ts
3158
3251
  var ARC_KEYS = ["references", "payload", "payloads", "inherits", "specializes"];
3159
3252
  var STRIP_KEYS = [...ARC_KEYS, "variants", "variantSets"];
3160
- async function composeLayer(text, baseUrl, resolver, options = {}, stack = /* @__PURE__ */ new Set()) {
3161
- return composeFile(parseUsda(text), baseUrl, resolver, options, stack);
3253
+ async function composeLayer(text, baseUrl, resolver, options = {}, stack = /* @__PURE__ */ new Set(), cache = /* @__PURE__ */ new Map()) {
3254
+ return composeFile(parseUsda(text), baseUrl, resolver, options, stack, cache);
3162
3255
  }
3163
- async function composeFile(file, baseUrl, resolver, options = {}, stack = /* @__PURE__ */ new Set()) {
3256
+ async function composeFile(file, baseUrl, resolver, options = {}, stack = /* @__PURE__ */ new Set(), cache = /* @__PURE__ */ new Map()) {
3164
3257
  const warn = options.onWarn ?? (() => {
3165
3258
  });
3166
3259
  let weak = [];
3167
3260
  const subLayers = toArcs(file.metadata.subLayers);
3168
3261
  for (let i = subLayers.length - 1; i >= 0; i--) {
3169
- const sub = await loadExternalFile(subLayers[i], baseUrl, resolver, options, stack, warn);
3262
+ const sub = await loadExternalFile(
3263
+ subLayers[i],
3264
+ baseUrl,
3265
+ resolver,
3266
+ options,
3267
+ stack,
3268
+ warn,
3269
+ cache
3270
+ );
3170
3271
  if (sub) weak = mergePrimLists(weak, sub.prims);
3171
3272
  }
3172
3273
  const ctx = {
@@ -3176,7 +3277,8 @@ async function composeFile(file, baseUrl, resolver, options = {}, stack = /* @__
3176
3277
  warn,
3177
3278
  stack,
3178
3279
  index: buildPathIndex(file.prims),
3179
- resolving: /* @__PURE__ */ new Set()
3280
+ resolving: /* @__PURE__ */ new Set(),
3281
+ cache
3180
3282
  };
3181
3283
  const resolved = [];
3182
3284
  for (const prim of file.prims) resolved.push(await resolvePrim(prim, "", ctx));
@@ -3187,6 +3289,7 @@ async function resolvePrim(spec, parentPath, ctx) {
3187
3289
  const path = `${parentPath}/${spec.name}`;
3188
3290
  let properties = spec.properties;
3189
3291
  let children = spec.children;
3292
+ const variantArcs = [];
3190
3293
  const selection = spec.metadata.variants;
3191
3294
  if (spec.variantSets && isDictionary2(selection)) {
3192
3295
  for (const [setName, variantName] of Object.entries(selection)) {
@@ -3194,6 +3297,7 @@ async function resolvePrim(spec, parentPath, ctx) {
3194
3297
  if (!variant) continue;
3195
3298
  properties = mergeProperties(variant.properties, properties);
3196
3299
  children = mergePrimLists(variant.children, children);
3300
+ for (const key of ARC_KEYS) variantArcs.push(...toArcs(variant.metadata[key]));
3197
3301
  }
3198
3302
  }
3199
3303
  const resolvedChildren = [];
@@ -3207,7 +3311,7 @@ async function resolvePrim(spec, parentPath, ctx) {
3207
3311
  children: resolvedChildren,
3208
3312
  line: spec.line
3209
3313
  };
3210
- const arcs = ARC_KEYS.flatMap((k) => toArcs(spec.metadata[k]));
3314
+ const arcs = [...variantArcs, ...ARC_KEYS.flatMap((k) => toArcs(spec.metadata[k]))];
3211
3315
  let base = null;
3212
3316
  for (const arc of arcs) {
3213
3317
  const target = await loadReferencedPrim(arc, ctx, path);
@@ -3216,20 +3320,21 @@ async function resolvePrim(spec, parentPath, ctx) {
3216
3320
  return base ? mergePrim(base, local) : local;
3217
3321
  }
3218
3322
  async function loadReferencedPrim(arc, ctx, destPath) {
3219
- if (arc.assetPath) {
3323
+ if (arc.assetPath?.path) {
3220
3324
  const composed = await loadExternalFile(
3221
3325
  arc,
3222
3326
  ctx.baseUrl,
3223
3327
  ctx.resolver,
3224
3328
  ctx.options,
3225
3329
  ctx.stack,
3226
- ctx.warn
3330
+ ctx.warn,
3331
+ ctx.cache
3227
3332
  );
3228
3333
  if (!composed) return null;
3229
3334
  const target = arc.primPath ? findPrimByPath(composed, arc.primPath) : defaultPrim(composed, ctx.warn);
3230
3335
  if (!target) {
3231
3336
  ctx.warn(
3232
- `reference target ${arc.primPath ?? "(defaultPrim)"} not found in ${arc.assetPath.path}`
3337
+ `reference target ${arc.primPath ?? "(defaultPrim)"} not found in ${arc.assetPath?.path}`
3233
3338
  );
3234
3339
  return null;
3235
3340
  }
@@ -3255,9 +3360,10 @@ async function loadReferencedPrim(arc, ctx, destPath) {
3255
3360
  }
3256
3361
  return null;
3257
3362
  }
3258
- async function loadExternalFile(arc, baseUrl, resolver, options, stack, warn) {
3259
- if (!arc.assetPath) return null;
3260
- const url = resolver.resolve(arc.assetPath.path, baseUrl);
3363
+ async function loadExternalFile(arc, baseUrl, resolver, options, stack, warn, cache) {
3364
+ if (!arc.assetPath?.path) return null;
3365
+ const assetPath = arc.assetPath.path;
3366
+ const url = resolver.resolve(assetPath, baseUrl);
3261
3367
  if (stack.has(url)) {
3262
3368
  warn(`composition cycle detected at ${url}; skipping`);
3263
3369
  return null;
@@ -3266,18 +3372,31 @@ async function loadExternalFile(arc, baseUrl, resolver, options, stack, warn) {
3266
3372
  warn(`composition exceeded max depth at ${url}; skipping`);
3267
3373
  return null;
3268
3374
  }
3269
- let bytes;
3270
- try {
3271
- bytes = await fetchLayerBytes(resolver, url);
3272
- } catch (err) {
3273
- warn(`cannot resolve "${arc.assetPath.path}" -> ${url}: ${err.message}`);
3274
- return null;
3275
- }
3276
- const childStack = /* @__PURE__ */ new Set([...stack, url]);
3277
- if (CrateReader.isCrate(bytes)) {
3278
- return composeFile(crateToUsdaFile(new CrateReader(bytes)), url, resolver, options, childStack);
3279
- }
3280
- return composeLayer(new TextDecoder().decode(bytes), url, resolver, options, childStack);
3375
+ const cached = cache.get(url);
3376
+ if (cached) return cached;
3377
+ const pending = (async () => {
3378
+ let bytes;
3379
+ try {
3380
+ bytes = await fetchLayerBytes(resolver, url);
3381
+ } catch (err) {
3382
+ warn(`cannot resolve "${assetPath}" -> ${url}: ${err.message}`);
3383
+ return null;
3384
+ }
3385
+ const childStack = /* @__PURE__ */ new Set([...stack, url]);
3386
+ if (CrateReader.isCrate(bytes)) {
3387
+ return composeFile(
3388
+ crateToUsdaFile(new CrateReader(bytes)),
3389
+ url,
3390
+ resolver,
3391
+ options,
3392
+ childStack,
3393
+ cache
3394
+ );
3395
+ }
3396
+ return composeLayer(new TextDecoder().decode(bytes), url, resolver, options, childStack, cache);
3397
+ })();
3398
+ cache.set(url, pending);
3399
+ return pending;
3281
3400
  }
3282
3401
  async function fetchLayerBytes(resolver, url) {
3283
3402
  if (resolver.fetchBytes) return resolver.fetchBytes(url);
@@ -3455,6 +3574,6 @@ function openUsdz(bytes) {
3455
3574
  return { rootEntry, resolver };
3456
3575
  }
3457
3576
 
3458
- export { ARTICULATION_ROOT_API, AssetPath, Attribute, COLLISION_API, CrateReader, DEFAULT_METERS_PER_UNIT, DEG2RAD, DefaultAssetResolver, Layer, MASS_API, MESH_COLLISION_API, PHYSICS_MATERIAL_API, ParseError, Prim, Quat, RAD2DEG, RIGID_BODY_API, Relationship, Stage, TokenizeError, UsdMatrix, buildKinematicTree, channelFromSamples, composeFile, composeLayer, computeLocalTransform, computeWorldTransform, crateToUsdaFile, createMemoryResolver, decomposeRigid, driveKindFor, extractRobotDescription, fromUsdMatrix, gatherMeshDescendants, getJointAxis, getJointBodies, getJointDrive, getJointLimits, getJointLocalFrame, getJointStatePosition, getJointType, getMassProperties, getTranslation, hasArticulationRootAPI, hasCollisionAPI, hasRigidBodyAPI, identity4, interpolate, invert, isMesh, isNonVisualPurpose, isScope, isXform, iterDescendants, joinPosix, jointValueFromSI, jointValueToSI, makeEuler, makeRotationFromQuat, makeRotationX, makeRotationY, makeRotationZ, makeScale, makeTranslation, multiply, multiplyAll, normalizeJointLimits, openUsdz, parseOpType, parseUsda, refineJointType, resolveBoundMaterial, serializeUsda, toUsdMatrix, tokenize };
3459
- //# sourceMappingURL=chunk-4MEZM763.js.map
3460
- //# sourceMappingURL=chunk-4MEZM763.js.map
3577
+ export { ARTICULATION_ROOT_API, AssetPath, Attribute, COLLISION_API, CrateReader, DEFAULT_METERS_PER_UNIT, DEG2RAD, DefaultAssetResolver, Layer, MASS_API, MESH_COLLISION_API, PHYSICS_MATERIAL_API, ParseError, Prim, Quat, RAD2DEG, RIGID_BODY_API, Relationship, Stage, TokenizeError, UsdMatrix, buildKinematicTree, channelFromSamples, composeFile, composeLayer, computeLocalTransform, computeWorldTransform, crateToUsdaFile, createMemoryResolver, decomposeRigid, driveKindFor, extractRobotDescription, fromUsdMatrix, gatherMeshDescendants, getJointAxis, getJointBodies, getJointDrive, getJointLimits, getJointLocalFrame, getJointStatePosition, getJointType, getMassProperties, getMaterialSubsets, getTranslation, hasArticulationRootAPI, hasCollisionAPI, hasRigidBodyAPI, identity4, interpolate, invert, isMesh, isNonVisualPurpose, isScope, isXform, iterDescendants, joinPosix, jointValueFromSI, jointValueToSI, makeEuler, makeRotationFromQuat, makeRotationX, makeRotationY, makeRotationZ, makeScale, makeTranslation, multiply, multiplyAll, normalizeJointLimits, openUsdz, parseOpType, parseUsda, refineJointType, resolveBoundMaterial, serializeUsda, toUsdMatrix, tokenize };
3578
+ //# sourceMappingURL=chunk-4GPCBXUS.js.map
3579
+ //# sourceMappingURL=chunk-4GPCBXUS.js.map