@needle-tools/usd 0.0.2-next.de2e82b → 1.0.0-next.d840b4c
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 +35 -1
- package/README.md +245 -28
- package/package.json +43 -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 +365 -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/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
|
+
}
|
|
294
|
+
|
|
295
|
+
return drawPromise;
|
|
296
|
+
};
|
|
158
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,104 @@ export async function createThreeHydra(config) {
|
|
|
178
397
|
}
|
|
179
398
|
return;
|
|
180
399
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
400
|
+
if (playing) {
|
|
401
|
+
time += dt;
|
|
402
|
+
const startTimeCode = stageStartTimeCode;
|
|
403
|
+
const duration = stageDuration();
|
|
404
|
+
let timecode = startTimeCode + time * stageTimeCodesPerSecond;
|
|
405
|
+
if (duration > 0) {
|
|
406
|
+
timecode = startTimeCode + ((timecode - startTimeCode) % duration);
|
|
407
|
+
currentTimeCode = timecode;
|
|
408
|
+
driver.SetTime(timecode);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
draw();
|
|
412
|
+
},
|
|
413
|
+
setTime: async (timeCode) => {
|
|
414
|
+
if (disposed || driver.isDeleted()) {
|
|
415
|
+
return Promise.resolve();
|
|
416
|
+
}
|
|
417
|
+
await drawPromise;
|
|
418
|
+
setHydraTime(timeCode);
|
|
419
|
+
return draw(true);
|
|
420
|
+
},
|
|
421
|
+
getTime: () => currentTimeCode,
|
|
422
|
+
setPlaying: (value) => {
|
|
423
|
+
playing = Boolean(value);
|
|
424
|
+
},
|
|
425
|
+
isPlaying: () => playing,
|
|
426
|
+
refresh: () => draw(),
|
|
427
|
+
setIncludedPurposes: async (includedPurposes) => {
|
|
428
|
+
if (disposed || driver.isDeleted() || typeof driver.SetIncludedPurposes !== "function") {
|
|
429
|
+
return Promise.resolve();
|
|
430
|
+
}
|
|
431
|
+
await drawPromise;
|
|
432
|
+
driver.SetIncludedPurposes(includedPurposes);
|
|
433
|
+
return draw(true);
|
|
434
|
+
},
|
|
435
|
+
editStage: async (callback) => {
|
|
436
|
+
if (disposed || driver.isDeleted()) {
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
await drawPromise;
|
|
440
|
+
if (drawInFlight) {
|
|
441
|
+
console.warn("Skipping USD stage edit while Hydra draw is still pending.");
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
if (disposed || driver.isDeleted()) {
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
const stage = await waitMaybeAsync(driver.GetStage());
|
|
448
|
+
editInFlight = true;
|
|
449
|
+
try {
|
|
450
|
+
const result = await callback(stage, driver);
|
|
451
|
+
if (disposed || driver.isDeleted()) {
|
|
452
|
+
return result;
|
|
453
|
+
}
|
|
454
|
+
await withTimeout(waitMaybeAsync(driver.Repopulate()), "Hydra repopulate");
|
|
455
|
+
editInFlight = false;
|
|
456
|
+
await draw(true);
|
|
457
|
+
return result;
|
|
458
|
+
}
|
|
459
|
+
finally {
|
|
460
|
+
editInFlight = false;
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
repopulate: async () => {
|
|
464
|
+
if (disposed || driver.isDeleted()) {
|
|
465
|
+
return Promise.resolve();
|
|
466
|
+
}
|
|
467
|
+
await withTimeout(waitMaybeAsync(driver.Repopulate()), "Hydra repopulate");
|
|
468
|
+
return draw();
|
|
469
|
+
},
|
|
470
|
+
materialsReady: () => renderInterface.waitForMaterialsReady(),
|
|
471
|
+
diagnostics: () => renderInterface.getDiagnostics(),
|
|
472
|
+
stageMetadata: () => {
|
|
473
|
+
applyStageMetadata(readStageMetadata());
|
|
474
|
+
return {
|
|
475
|
+
upAxis: String.fromCharCode(stageUpAxis),
|
|
476
|
+
startTimeCode: stageStartTimeCode,
|
|
477
|
+
endTimeCode: stageEndTimeCode,
|
|
478
|
+
timeCodesPerSecond: stageTimeCodesPerSecond,
|
|
479
|
+
};
|
|
186
480
|
},
|
|
187
481
|
/**
|
|
188
|
-
*
|
|
482
|
+
* Dispose the Three Hydra delegate.
|
|
189
483
|
* This does *not* clear the threejs scene but only dispose the USD delegate and loaded files
|
|
190
484
|
*/
|
|
191
|
-
dispose: () => {
|
|
485
|
+
dispose: async () => {
|
|
486
|
+
if (disposed) return;
|
|
487
|
+
disposed = true;
|
|
192
488
|
if (debug) console.warn("Disposing Three Hydra");
|
|
193
489
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
490
|
+
const cleanup = async () => {
|
|
491
|
+
await renderInterface.waitForMaterialsReady().catch(() => {});
|
|
492
|
+
renderInterface.dispose();
|
|
493
|
+
disposeObjectTree(scenePrimitiveRoot);
|
|
494
|
+
|
|
495
|
+
// Unlink all generated files and folders in the virtual file system.
|
|
496
|
+
const unlinkedFiles = new Set();
|
|
497
|
+
function unlinkFiles(dir, path) {
|
|
197
498
|
for (const fileName of Object.keys(dir.contents)) {
|
|
198
499
|
const file = dir.contents[fileName];
|
|
199
500
|
if (file.isFolder) {
|
|
@@ -206,7 +507,7 @@ export async function createThreeHydra(config) {
|
|
|
206
507
|
config.USD.FS_rmdir(path + fileName);
|
|
207
508
|
}
|
|
208
509
|
catch (e) {
|
|
209
|
-
console.
|
|
510
|
+
if (debug) console.debug("Error unlinking folder", fullPath, e);
|
|
210
511
|
}
|
|
211
512
|
}
|
|
212
513
|
else {
|
|
@@ -216,37 +517,48 @@ export async function createThreeHydra(config) {
|
|
|
216
517
|
config.USD.FS_unlink(fullPath);
|
|
217
518
|
}
|
|
218
519
|
catch (e) {
|
|
219
|
-
console.
|
|
520
|
+
if (debug) console.debug("Error unlinking", fullPath, e);
|
|
220
521
|
}
|
|
221
522
|
}
|
|
222
523
|
}
|
|
223
|
-
|
|
524
|
+
}
|
|
224
525
|
|
|
225
|
-
|
|
526
|
+
function rmRootDir(rootDir) {
|
|
226
527
|
const allFiles = config.USD.FS_analyzePath(rootDir).object;
|
|
227
528
|
if (allFiles)
|
|
228
529
|
unlinkFiles(allFiles, rootDir);
|
|
229
|
-
|
|
530
|
+
}
|
|
230
531
|
|
|
231
|
-
|
|
232
|
-
|
|
532
|
+
rmRootDir("/" + directoryForFiles);
|
|
533
|
+
rmRootDir("/1/"); // HTTPAssetResolver puts files into a series of folders named "/1/1/1/1" to allow for parent traversal
|
|
233
534
|
|
|
234
|
-
|
|
535
|
+
if (!unlinkedFiles.has(file)) {
|
|
235
536
|
if (debug) console.warn("Unlinking main file", file);
|
|
236
537
|
let fileToUnlink = file;
|
|
237
538
|
if (fileToUnlink.startsWith("http"))
|
|
238
539
|
fileToUnlink = "/" + fileToUnlink.replace("://", ":/");
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
540
|
+
if (config.USD.FS_analyzePath(fileToUnlink)?.exists) {
|
|
541
|
+
try {
|
|
542
|
+
config.USD.FS_unlink(fileToUnlink);
|
|
543
|
+
} catch (e) {
|
|
544
|
+
if (debug) console.debug("Error unlinking main file", fileToUnlink, e);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
243
547
|
}
|
|
244
|
-
}
|
|
245
548
|
|
|
246
|
-
|
|
247
|
-
|
|
549
|
+
if (!driver.isDeleted()) {
|
|
550
|
+
driver.delete();
|
|
551
|
+
}
|
|
552
|
+
driverOrPromise = null;
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
if (drawInFlight) {
|
|
556
|
+
console.warn("Waiting for pending Hydra draw before USD cleanup.");
|
|
557
|
+
}
|
|
558
|
+
await drawPromise.catch(() => {});
|
|
559
|
+
await cleanup();
|
|
248
560
|
|
|
249
561
|
if (debug) console.warn("Disposed Three Hydra");
|
|
250
562
|
},
|
|
251
563
|
}
|
|
252
|
-
}
|
|
564
|
+
}
|