@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.
- package/CHANGELOG.md +36 -1
- package/README.md +245 -28
- package/package.json +46 -10
- package/src/bindings/emHdBindings.js +5 -12227
- package/src/bindings/emHdBindings.wasm +0 -0
- package/src/bindings/index.js +130 -47
- package/src/bindings/openusd-build-info.json +40 -0
- package/src/create.three.js +368 -53
- package/src/hydra/ThreeJsRenderDelegate.js +1128 -75
- package/src/plugins/index.js +1 -2
- package/src/plugins/needle.js +38 -2
- package/src/types/bindings.d.ts +296 -3
- package/src/types/create.three.d.ts +87 -7
- package/src/types/hydra.d.ts +7 -5
- package/src/types/plugins.d.ts +7 -0
- package/src/types/usd-core-bindings.d.ts +240 -0
- package/src/utils.js +3 -3
- package/src/vite/index.js +13 -1
- package/examples/index.html +0 -58
- package/examples/package-lock.json +0 -1548
- package/examples/package.json +0 -24
- package/examples/public/HttpReferences copy.usda +0 -46
- package/examples/public/HttpReferences.usda +0 -44
- package/examples/public/gingerbread/GingerbreadHouse.usda +0 -35
- package/examples/public/gingerbread/house/GingerBreadHouse.usdc +0 -0
- package/examples/public/gingerbread/house/textures/color.jpg +0 -0
- package/examples/public/gingerbread/house/textures/metallic_roughness.jpg +0 -0
- package/examples/public/gingerbread/house/textures/normal.jpg +0 -0
- package/examples/public/gingerbread/snowman/Snowman.usdc +0 -0
- package/examples/public/gingerbread/snowman/textures/color.jpg +0 -0
- package/examples/public/gingerbread/snowman/textures/metallic_roughness.jpg +0 -0
- package/examples/public/gingerbread/snowman/textures/normal.jpg +0 -0
- package/examples/public/test.usdz +0 -0
- package/examples/public/vite.svg +0 -1
- package/examples/src/fileHandling.ts +0 -256
- package/examples/src/main.ts +0 -167
- package/examples/src/three.ts +0 -140
- package/examples/src/vite-env.d.ts +0 -1
- package/examples/tsconfig.json +0 -23
- package/examples/vite.config.js +0 -21
- package/src/bindings/emHdBindings.data +0 -19331
- package/src/bindings/emHdBindings.worker.js +0 -124
package/src/create.three.js
CHANGED
|
@@ -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
|
|
16
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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.
|
|
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.
|
|
523
|
+
if (debug) console.debug("Error unlinking", fullPath, e);
|
|
220
524
|
}
|
|
221
525
|
}
|
|
222
526
|
}
|
|
223
|
-
|
|
527
|
+
}
|
|
224
528
|
|
|
225
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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
|
+
}
|