@needle-tools/usd 0.0.2-next.d90870e → 1.0.0-next.4d692f6

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/CHANGELOG.md +36 -1
  2. package/README.md +245 -28
  3. package/package.json +46 -10
  4. package/src/bindings/emHdBindings.js +5 -12227
  5. package/src/bindings/emHdBindings.wasm +0 -0
  6. package/src/bindings/index.js +130 -47
  7. package/src/bindings/openusd-build-info.json +40 -0
  8. package/src/create.three.js +368 -53
  9. package/src/hydra/ThreeJsRenderDelegate.js +1128 -75
  10. package/src/plugins/index.js +1 -2
  11. package/src/plugins/needle.js +38 -2
  12. package/src/types/bindings.d.ts +296 -3
  13. package/src/types/create.three.d.ts +87 -7
  14. package/src/types/hydra.d.ts +7 -5
  15. package/src/types/plugins.d.ts +7 -0
  16. package/src/types/usd-core-bindings.d.ts +240 -0
  17. package/src/utils.js +3 -3
  18. package/src/vite/index.js +13 -1
  19. package/examples/index.html +0 -58
  20. package/examples/package-lock.json +0 -1548
  21. package/examples/package.json +0 -24
  22. package/examples/public/HttpReferences copy.usda +0 -46
  23. package/examples/public/HttpReferences.usda +0 -44
  24. package/examples/public/gingerbread/GingerbreadHouse.usda +0 -35
  25. package/examples/public/gingerbread/house/GingerBreadHouse.usdc +0 -0
  26. package/examples/public/gingerbread/house/textures/color.jpg +0 -0
  27. package/examples/public/gingerbread/house/textures/metallic_roughness.jpg +0 -0
  28. package/examples/public/gingerbread/house/textures/normal.jpg +0 -0
  29. package/examples/public/gingerbread/snowman/Snowman.usdc +0 -0
  30. package/examples/public/gingerbread/snowman/textures/color.jpg +0 -0
  31. package/examples/public/gingerbread/snowman/textures/metallic_roughness.jpg +0 -0
  32. package/examples/public/gingerbread/snowman/textures/normal.jpg +0 -0
  33. package/examples/public/test.usdz +0 -0
  34. package/examples/public/vite.svg +0 -1
  35. package/examples/src/fileHandling.ts +0 -256
  36. package/examples/src/main.ts +0 -167
  37. package/examples/src/three.ts +0 -140
  38. package/examples/src/vite-env.d.ts +0 -1
  39. package/examples/tsconfig.json +0 -23
  40. package/examples/vite.config.js +0 -21
  41. package/src/bindings/emHdBindings.data +0 -19331
  42. package/src/bindings/emHdBindings.worker.js +0 -124
@@ -1,6 +1,79 @@
1
1
  import { threeJsRenderDelegate } from "./hydra/index.js";
2
2
  import { tryDetermineFileFormat } from "./utils.js";
3
+ import { Object3D } from "three";
3
4
 
5
+ /**
6
+ * @param {string} url
7
+ */
8
+ function toBrowserFetchableUrl(url) {
9
+ if (url.startsWith("https://github.com/") || url.startsWith("http://github.com/")) {
10
+ url = url.replace("github.com", "raw.githubusercontent.com");
11
+ url = url.replace("/blob/", "/");
12
+ }
13
+
14
+ if (url.startsWith("http") || url.startsWith("blob")) {
15
+ return url;
16
+ }
17
+
18
+ if (globalThis.location?.href) {
19
+ return new URL(url, globalThis.location.href).href;
20
+ }
21
+
22
+ return url;
23
+ }
24
+
25
+ /**
26
+ * @param {unknown} value
27
+ * @returns {value is Promise<unknown>}
28
+ */
29
+ function isPromiseLike(value) {
30
+ return !!value && typeof value === "object" && "then" in value;
31
+ }
32
+
33
+ /**
34
+ * @param {string} url
35
+ */
36
+ function isUsdPackageUrl(url) {
37
+ const path = url.split(/[?#]/, 1)[0].toLowerCase();
38
+ return path.endsWith(".usdz");
39
+ }
40
+
41
+ /**
42
+ * @template T
43
+ * @param {T | Promise<T>} value
44
+ * @returns {Promise<T>}
45
+ */
46
+ async function waitMaybeAsync(value) {
47
+ return await value;
48
+ }
49
+
50
+ /**
51
+ * @param {Promise<unknown>} promise
52
+ * @param {string} label
53
+ * @param {number} timeoutMs
54
+ */
55
+ function withTimeout(promise, label, timeoutMs = 15000) {
56
+ let timeout = 0;
57
+ const timeoutPromise = new Promise((resolve) => {
58
+ timeout = setTimeout(() => {
59
+ console.warn(`${label} is still pending after ${timeoutMs}ms.`);
60
+ resolve(undefined);
61
+ }, timeoutMs);
62
+ });
63
+ return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeout));
64
+ }
65
+
66
+ function disposeObjectTree(root) {
67
+ if (!root) return;
68
+ root.traverse?.((object) => {
69
+ object.geometry?.dispose?.();
70
+ const materials = Array.isArray(object.material) ? object.material : [object.material];
71
+ for (const material of materials) {
72
+ material?.dispose?.();
73
+ }
74
+ });
75
+ root.parent?.remove(root);
76
+ }
4
77
 
5
78
  /**
6
79
  * @param {{USD:import("./types").USD, filepath:string, buffer?:ArrayBuffer, parent?:string,}} opts
@@ -9,11 +82,15 @@ async function createFile(opts) {
9
82
  if (typeof opts.filepath !== "string") throw new Error("Filepath must be a string");
10
83
 
11
84
  let filepath = /** @type {string} */ (opts.filepath);
85
+ let parent = opts.parent || "";
12
86
 
13
87
  let arrayBuffer = opts.buffer;
14
88
  if (!arrayBuffer) {
15
- const blob = await fetch(filepath);
16
- arrayBuffer = await blob.arrayBuffer();
89
+ const response = await fetch(filepath);
90
+ if (!response.ok) {
91
+ throw new Error(`Failed to fetch USD file ${filepath}: ${response.status} ${response.statusText}`);
92
+ }
93
+ arrayBuffer = await response.arrayBuffer();
17
94
  }
18
95
 
19
96
  // ensure that file paths are not using slashes
@@ -22,8 +99,8 @@ async function createFile(opts) {
22
99
 
23
100
  const format = tryDetermineFileFormat(arrayBuffer);
24
101
  const ext = filepath.split(".").pop();
25
- if (ext !== "usdz" && ext !== "usd" && ext !== "usda") {
26
- if (format === "usdz" || format === "usd" || format === "usda") {
102
+ if (ext !== "usdz" && ext !== "usd" && ext !== "usda" && ext !== "usdc") {
103
+ if (format === "usdz" || format === "usd" || format === "usda" || format === "usdc") {
27
104
  filepath += "." + format;
28
105
  } else {
29
106
  console.warn("Unknown file format - assuming .usdz");
@@ -34,10 +111,17 @@ async function createFile(opts) {
34
111
  console.warn("File extension does not match file format", { ext, format });
35
112
  }
36
113
 
114
+ if (parent) {
115
+ parent = parent.replaceAll(/\\/g, "/");
116
+ if (parent.startsWith("/")) parent = parent.slice(1);
117
+ if (!parent.endsWith("/")) parent += "/";
118
+ opts.USD.FS_createPath("", parent, true, true);
119
+ }
120
+
37
121
  // Put a simple USDZ file into the virtual file system so USD can access it
38
122
  // Create a file in the virtual file system
39
- opts.USD.FS_createDataFile("", filepath, new Uint8Array(arrayBuffer), true, true, true);
40
- return filepath;
123
+ opts.USD.FS_createDataFile(parent, filepath, new Uint8Array(arrayBuffer), true, true, true);
124
+ return parent + filepath;
41
125
  }
42
126
 
43
127
  export class USDLoadingManager {
@@ -66,6 +150,7 @@ export async function createThreeHydra(config) {
66
150
  // Some common directory is needed so that we don't get clashes with root-level files
67
151
  // and directories in the virtual file system
68
152
  const directoryForFiles = "needle/";
153
+ const loadedFilePaths = [];
69
154
 
70
155
  // We're loading all provided files into the virtual file system.
71
156
  // Potentially, we could also resolve dropped files on the fly and load them only when needed,
@@ -73,7 +158,7 @@ export async function createThreeHydra(config) {
73
158
  if (Array.isArray(config.files)) {
74
159
  for (const file of config.files) {
75
160
  let fileName = file.name;
76
- let directory = "/";
161
+ let directory = "";
77
162
  if (file.path) {
78
163
  const parts = file.path.split('/');
79
164
  if (parts.length > 1) {
@@ -83,7 +168,13 @@ export async function createThreeHydra(config) {
83
168
  }
84
169
 
85
170
  USD.FS_createPath("", directoryForFiles + directory, true, true);
86
- USD.FS_createDataFile(directoryForFiles + directory, fileName, new Uint8Array(await file.arrayBuffer()), true, true, true);
171
+ const fileBuffer = await file.arrayBuffer();
172
+ const bytes = new Uint8Array(fileBuffer);
173
+ USD.FS_createDataFile(directoryForFiles + directory, fileName, bytes, true, true, true);
174
+ if (file.path) {
175
+ loadedFilePaths.push(directoryForFiles + file.path);
176
+ loadedFilePaths.push(file.path);
177
+ }
87
178
  }
88
179
  }
89
180
 
@@ -98,19 +189,26 @@ export async function createThreeHydra(config) {
98
189
  // - when a blob is provided, we create a file from that blob and sanitize the filename.
99
190
  let file = "";
100
191
  if (config.files?.length) {
101
- file = directoryForFiles + config.files[0].path;
192
+ file = "/" + directoryForFiles + config.files[0].path;
102
193
  }
103
194
  else if (config.url) {
104
- const isBlob = config.url.startsWith("blob");
105
- const isWebUrl = config.url.startsWith("http");
106
- if (isBlob) {
107
- file = await createFile({ USD, filepath: config.url, buffer });
108
- }
109
- else if ((allowFetchWebUrls && isWebUrl) || allowFetchLocalFiles) {
110
- file = config.url;
195
+ if (buffer) {
196
+ file = await createFile({ USD, filepath: config.url, buffer, parent: directoryForFiles });
197
+ if (!file.startsWith("/")) file = "/" + file;
111
198
  }
112
199
  else {
113
- file = await createFile({ USD, filepath: config.url, buffer });
200
+ const resolvedUrl = toBrowserFetchableUrl(config.url);
201
+ const isBlob = resolvedUrl.startsWith("blob");
202
+ const isWebUrl = resolvedUrl.startsWith("http");
203
+ if (isBlob || isUsdPackageUrl(resolvedUrl)) {
204
+ file = await createFile({ USD, filepath: resolvedUrl });
205
+ }
206
+ else if ((allowFetchWebUrls && isWebUrl) || allowFetchLocalFiles) {
207
+ file = resolvedUrl;
208
+ }
209
+ else {
210
+ file = await createFile({ USD, filepath: resolvedUrl });
211
+ }
114
212
  }
115
213
  }
116
214
 
@@ -125,12 +223,23 @@ export async function createThreeHydra(config) {
125
223
  */
126
224
  let driverOrPromise = null;
127
225
 
226
+ const scenePrimitiveRoot = new Object3D();
227
+ scenePrimitiveRoot.name = "__usd_scene_primitives";
228
+ scenePrimitiveRoot.userData.usdScenePrimitiveRoot = true;
229
+ config.scene.add(scenePrimitiveRoot);
230
+
128
231
  /**
129
232
  * @type {import(".").threeJsRenderDelegateConfig}
130
233
  */
131
234
  const delegateConfig = {
132
235
  usdRoot: config.scene,
133
- paths: new Array(),
236
+ scenePrimitiveRoot,
237
+ scenePrimitiveLightIntensityScale: config.scenePrimitiveLightIntensityScale,
238
+ showScenePrimitiveHelpers: config.showScenePrimitiveHelpers,
239
+ showCameraHelpers: config.showCameraHelpers,
240
+ showLightHelpers: config.showLightHelpers,
241
+ paths: loadedFilePaths,
242
+ USD,
134
243
  driver: () => /** @type {import(".").HdWebSyncDriver} */(driverOrPromise),
135
244
  };
136
245
 
@@ -144,30 +253,140 @@ export async function createThreeHydra(config) {
144
253
  }
145
254
 
146
255
  const driver = /** @type {import(".").HdWebSyncDriver} */ (driverOrPromise);
256
+ if (typeof driver.HasStage === "function" && !driver.HasStage()) {
257
+ driver.delete();
258
+ throw new Error(`Failed to open USD stage: ${file}`);
259
+ }
260
+
261
+ if (typeof driver.SetIncludedPurposes === "function") {
262
+ driver.SetIncludedPurposes(config.includedPurposes ?? ["default", "render"]);
263
+ }
147
264
 
148
265
  if (debug) console.log("DRIVER", driver);
149
266
 
150
- /** Draw once */
151
- driver.Draw();
267
+ let disposed = false;
268
+ let drawInFlight = false;
269
+ let editInFlight = false;
270
+ let drawPromise = Promise.resolve();
271
+ let activeDrawPromise = Promise.resolve();
272
+ const draw = (force = false) => {
273
+ if (disposed || drawInFlight || driver.isDeleted() || (editInFlight && !force)) {
274
+ return drawPromise;
275
+ }
152
276
 
153
- let stage = driver.GetStage();
154
- if (stage instanceof Promise) {
155
- stage = await stage;
156
- stage = driver.GetStage();
157
- }
277
+ try {
278
+ const result = driver.Draw();
279
+ if (isPromiseLike(result)) {
280
+ drawInFlight = true;
281
+ activeDrawPromise = result
282
+ .catch((error) => console.error("Hydra draw failed", error))
283
+ .finally(() => drawInFlight = false);
284
+ drawPromise = withTimeout(activeDrawPromise, "Hydra draw");
285
+ }
286
+ else {
287
+ drawPromise = Promise.resolve();
288
+ }
289
+ }
290
+ catch (error) {
291
+ console.error("Hydra draw failed", error);
292
+ drawPromise = Promise.resolve();
293
+ }
158
294
 
295
+ return drawPromise;
296
+ };
297
+
298
+ const stage = await waitMaybeAsync(driver.GetStage());
299
+ const requireStageMethod = (name) => {
300
+ if (!stage || typeof stage[name] !== "function") {
301
+ throw new Error(`OpenUSD stage API is missing ${name}; cannot read stage metadata`);
302
+ }
303
+ return stage[name].bind(stage);
304
+ };
305
+ const getStageUpAxis = requireStageMethod("GetUpAxis");
306
+ const getStageStartTimeCode = requireStageMethod("GetStartTimeCode");
307
+ const getStageEndTimeCode = requireStageMethod("GetEndTimeCode");
308
+ const getStageTimeCodesPerSecond = requireStageMethod("GetTimeCodesPerSecond");
159
309
  /** Support for Y and Z up-axis in the root USD file */
160
- delegateConfig.usdRoot.rotation.x = String.fromCharCode(stage.GetUpAxis()) === 'z' ? -Math.PI / 2 : 0;
310
+ let stageUpAxis = 0;
311
+ let stageStartTimeCode = 0;
312
+ let stageEndTimeCode = 0;
313
+ let stageTimeCodesPerSecond = 24;
314
+ const normalizeUpAxisToken = (axis) => {
315
+ if (typeof axis === "number" && Number.isFinite(axis)) {
316
+ return String.fromCharCode(axis).toLowerCase();
317
+ }
318
+ if (typeof axis === "string" && axis.length > 0) {
319
+ return axis[0].toLowerCase();
320
+ }
321
+ return "y";
322
+ };
323
+ const applyStageMetadata = (metadata) => {
324
+ const stageUpAxisToken = normalizeUpAxisToken(metadata.upAxis);
325
+ stageUpAxis = stageUpAxisToken.charCodeAt(0);
326
+ stageStartTimeCode = Number.isFinite(metadata.startTimeCode) ? metadata.startTimeCode : 0;
327
+ stageEndTimeCode = Number.isFinite(metadata.endTimeCode) ? metadata.endTimeCode : stageStartTimeCode;
328
+ stageTimeCodesPerSecond = metadata.timeCodesPerSecond > 0 ? metadata.timeCodesPerSecond : 24;
329
+ delegateConfig.usdRoot.rotation.x = stageUpAxisToken === 'z' ? -Math.PI / 2 : 0;
330
+ delegateConfig.usdRoot.updateMatrixWorld(true);
331
+ };
332
+ const readStageMetadata = () => {
333
+ if (disposed || driver.isDeleted()) {
334
+ return {
335
+ upAxis: stageUpAxis,
336
+ startTimeCode: stageStartTimeCode,
337
+ endTimeCode: stageEndTimeCode,
338
+ timeCodesPerSecond: stageTimeCodesPerSecond,
339
+ };
340
+ }
341
+ return {
342
+ upAxis: getStageUpAxis(),
343
+ startTimeCode: getStageStartTimeCode(),
344
+ endTimeCode: getStageEndTimeCode(),
345
+ timeCodesPerSecond: getStageTimeCodesPerSecond(),
346
+ };
347
+ };
348
+ applyStageMetadata(readStageMetadata());
349
+
350
+ /** Draw once, after stage metadata has been applied to the root scene. */
351
+ const initialDrawPromise = draw();
352
+ const readyPromise = config.waitForMaterials
353
+ ? initialDrawPromise.then(() => renderInterface.waitForMaterialsReady())
354
+ : initialDrawPromise;
161
355
 
162
356
  let time = 0;
357
+ let currentTimeCode = stageStartTimeCode;
358
+ let playing = true;
359
+
360
+ const stageDuration = () => stageEndTimeCode - stageStartTimeCode;
361
+
362
+ const clampStageTimeCode = (timeCode) => {
363
+ if (!Number.isFinite(timeCode)) return stageStartTimeCode;
364
+ const duration = stageDuration();
365
+ if (duration <= 0) return stageStartTimeCode;
366
+ return Math.min(stageEndTimeCode, Math.max(stageStartTimeCode, timeCode));
367
+ };
368
+
369
+ const setHydraTime = (timeCode) => {
370
+ currentTimeCode = clampStageTimeCode(timeCode);
371
+ time = stageTimeCodesPerSecond > 0
372
+ ? (currentTimeCode - stageStartTimeCode) / stageTimeCodesPerSecond
373
+ : 0;
374
+ driver.SetTime(currentTimeCode);
375
+ };
163
376
 
164
377
  if (debug) {
165
- console.log("STAGE", stage);
378
+ console.log("STAGE", {
379
+ upAxis: String.fromCharCode(stageUpAxis),
380
+ startTimeCode: stageStartTimeCode,
381
+ endTimeCode: stageEndTimeCode,
382
+ timeCodesPerSecond: stageTimeCodesPerSecond,
383
+ });
166
384
  console.log("VIRTUAL FILESYSTEM", USD.FS_analyzePath("/"));
167
385
  }
168
386
 
169
387
  return {
170
388
  driver: /** @type {import(".").HdWebSyncDriver} */ (driverOrPromise),
389
+ ready: () => readyPromise,
171
390
  update: (dt) => {
172
391
  // ensure we're not dead
173
392
  if (driver.isDeleted()) {
@@ -178,22 +397,107 @@ export async function createThreeHydra(config) {
178
397
  }
179
398
  return;
180
399
  }
181
- time += dt;
182
- let timecode = time * stage.GetTimeCodesPerSecond();
183
- timecode = timecode % (stage.GetEndTimeCode() - stage.GetStartTimeCode());
184
- driver.SetTime(timecode);
185
- driver.Draw();
400
+ if (drawInFlight || editInFlight) {
401
+ return;
402
+ }
403
+ if (playing) {
404
+ time += dt;
405
+ const startTimeCode = stageStartTimeCode;
406
+ const duration = stageDuration();
407
+ let timecode = startTimeCode + time * stageTimeCodesPerSecond;
408
+ if (duration > 0) {
409
+ timecode = startTimeCode + ((timecode - startTimeCode) % duration);
410
+ currentTimeCode = timecode;
411
+ driver.SetTime(timecode);
412
+ }
413
+ }
414
+ draw();
415
+ },
416
+ setTime: async (timeCode) => {
417
+ if (disposed || driver.isDeleted()) {
418
+ return Promise.resolve();
419
+ }
420
+ await drawPromise;
421
+ setHydraTime(timeCode);
422
+ return draw(true);
423
+ },
424
+ getTime: () => currentTimeCode,
425
+ setPlaying: (value) => {
426
+ playing = Boolean(value);
427
+ },
428
+ isPlaying: () => playing,
429
+ refresh: () => draw(),
430
+ setIncludedPurposes: async (includedPurposes) => {
431
+ if (disposed || driver.isDeleted() || typeof driver.SetIncludedPurposes !== "function") {
432
+ return Promise.resolve();
433
+ }
434
+ await drawPromise;
435
+ driver.SetIncludedPurposes(includedPurposes);
436
+ return draw(true);
437
+ },
438
+ editStage: async (callback) => {
439
+ if (disposed || driver.isDeleted()) {
440
+ return undefined;
441
+ }
442
+ await drawPromise;
443
+ if (drawInFlight) {
444
+ console.warn("Skipping USD stage edit while Hydra draw is still pending.");
445
+ return undefined;
446
+ }
447
+ if (disposed || driver.isDeleted()) {
448
+ return undefined;
449
+ }
450
+ const stage = await waitMaybeAsync(driver.GetStage());
451
+ editInFlight = true;
452
+ try {
453
+ const result = await callback(stage, driver);
454
+ if (disposed || driver.isDeleted()) {
455
+ return result;
456
+ }
457
+ await withTimeout(waitMaybeAsync(driver.Repopulate()), "Hydra repopulate");
458
+ editInFlight = false;
459
+ await draw(true);
460
+ return result;
461
+ }
462
+ finally {
463
+ editInFlight = false;
464
+ }
465
+ },
466
+ repopulate: async () => {
467
+ if (disposed || driver.isDeleted()) {
468
+ return Promise.resolve();
469
+ }
470
+ await withTimeout(waitMaybeAsync(driver.Repopulate()), "Hydra repopulate");
471
+ return draw();
472
+ },
473
+ materialsReady: () => renderInterface.waitForMaterialsReady(),
474
+ diagnostics: () => renderInterface.getDiagnostics(),
475
+ stageMetadata: () => {
476
+ applyStageMetadata(readStageMetadata());
477
+ return {
478
+ upAxis: String.fromCharCode(stageUpAxis),
479
+ startTimeCode: stageStartTimeCode,
480
+ endTimeCode: stageEndTimeCode,
481
+ timeCodesPerSecond: stageTimeCodesPerSecond,
482
+ };
186
483
  },
187
484
  /**
188
- * Dipoose the Three Hydra delegate.
485
+ * Dispose the Three Hydra delegate.
189
486
  * This does *not* clear the threejs scene but only dispose the USD delegate and loaded files
190
487
  */
191
- dispose: () => {
488
+ dispose: async () => {
489
+ if (disposed) return;
490
+ disposed = true;
192
491
  if (debug) console.warn("Disposing Three Hydra");
193
492
 
194
- // Unlink all generated files and folders in the virtual file system.
195
- const unlinkedFiles = new Set();
196
- function unlinkFiles(dir, path) {
493
+ const cleanup = async () => {
494
+ await renderInterface.waitForMaterialsReady().catch(() => {});
495
+ renderInterface.dispose();
496
+ disposeObjectTree(scenePrimitiveRoot);
497
+
498
+ // Unlink all generated files and folders in the virtual file system.
499
+ const unlinkedFiles = new Set();
500
+ function unlinkFiles(dir, path) {
197
501
  for (const fileName of Object.keys(dir.contents)) {
198
502
  const file = dir.contents[fileName];
199
503
  if (file.isFolder) {
@@ -206,7 +510,7 @@ export async function createThreeHydra(config) {
206
510
  config.USD.FS_rmdir(path + fileName);
207
511
  }
208
512
  catch (e) {
209
- console.error("Error unlinking folder", fullPath, e);
513
+ if (debug) console.debug("Error unlinking folder", fullPath, e);
210
514
  }
211
515
  }
212
516
  else {
@@ -216,37 +520,48 @@ export async function createThreeHydra(config) {
216
520
  config.USD.FS_unlink(fullPath);
217
521
  }
218
522
  catch (e) {
219
- console.error("Error unlinking", fullPath, e);
523
+ if (debug) console.debug("Error unlinking", fullPath, e);
220
524
  }
221
525
  }
222
526
  }
223
- }
527
+ }
224
528
 
225
- function rmRootDir(rootDir) {
529
+ function rmRootDir(rootDir) {
226
530
  const allFiles = config.USD.FS_analyzePath(rootDir).object;
227
531
  if (allFiles)
228
532
  unlinkFiles(allFiles, rootDir);
229
- }
533
+ }
230
534
 
231
- rmRootDir("/" + directoryForFiles);
232
- rmRootDir("/1/"); // HTTPAssetResolver puts files into a series of folders named "/1/1/1/1" to allow for parent traversal
535
+ rmRootDir("/" + directoryForFiles);
536
+ rmRootDir("/1/"); // HTTPAssetResolver puts files into a series of folders named "/1/1/1/1" to allow for parent traversal
233
537
 
234
- if (!unlinkedFiles.has(file)) {
538
+ if (!unlinkedFiles.has(file)) {
235
539
  if (debug) console.warn("Unlinking main file", file);
236
540
  let fileToUnlink = file;
237
541
  if (fileToUnlink.startsWith("http"))
238
542
  fileToUnlink = "/" + fileToUnlink.replace("://", ":/");
239
- try {
240
- config.USD.FS_unlink(fileToUnlink);
241
- } catch (e) {
242
- console.error("Error unlinking main file", fileToUnlink, e);
543
+ if (config.USD.FS_analyzePath(fileToUnlink)?.exists) {
544
+ try {
545
+ config.USD.FS_unlink(fileToUnlink);
546
+ } catch (e) {
547
+ if (debug) console.debug("Error unlinking main file", fileToUnlink, e);
548
+ }
549
+ }
243
550
  }
244
- }
245
551
 
246
- driver.delete();
247
- driverOrPromise = null;
552
+ if (!driver.isDeleted()) {
553
+ driver.delete();
554
+ }
555
+ driverOrPromise = null;
556
+ };
557
+
558
+ if (drawInFlight) {
559
+ console.warn("Waiting for pending Hydra draw before USD cleanup.");
560
+ }
561
+ await drawPromise.catch(() => {});
562
+ await cleanup();
248
563
 
249
564
  if (debug) console.warn("Disposed Three Hydra");
250
565
  },
251
566
  }
252
- }
567
+ }