@needle-tools/usd 0.0.2-next.d90870e → 1.0.0-next.d536d99
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 +12 -1
- package/README.md +52 -9
- package/package.json +42 -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 +362 -53
- package/src/hydra/ThreeJsRenderDelegate.js +959 -73
- package/src/plugins/index.js +1 -2
- package/src/plugins/needle.js +37 -2
- package/src/types/bindings.d.ts +296 -3
- package/src/types/create.three.d.ts +78 -7
- package/src/types/hydra.d.ts +7 -5
- package/src/types/plugins.d.ts +2 -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,137 @@ 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();
|
|
161
352
|
|
|
162
353
|
let time = 0;
|
|
354
|
+
let currentTimeCode = stageStartTimeCode;
|
|
355
|
+
let playing = true;
|
|
356
|
+
|
|
357
|
+
const stageDuration = () => stageEndTimeCode - stageStartTimeCode;
|
|
358
|
+
|
|
359
|
+
const clampStageTimeCode = (timeCode) => {
|
|
360
|
+
if (!Number.isFinite(timeCode)) return stageStartTimeCode;
|
|
361
|
+
const duration = stageDuration();
|
|
362
|
+
if (duration <= 0) return stageStartTimeCode;
|
|
363
|
+
return Math.min(stageEndTimeCode, Math.max(stageStartTimeCode, timeCode));
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const setHydraTime = (timeCode) => {
|
|
367
|
+
currentTimeCode = clampStageTimeCode(timeCode);
|
|
368
|
+
time = stageTimeCodesPerSecond > 0
|
|
369
|
+
? (currentTimeCode - stageStartTimeCode) / stageTimeCodesPerSecond
|
|
370
|
+
: 0;
|
|
371
|
+
driver.SetTime(currentTimeCode);
|
|
372
|
+
};
|
|
163
373
|
|
|
164
374
|
if (debug) {
|
|
165
|
-
console.log("STAGE",
|
|
375
|
+
console.log("STAGE", {
|
|
376
|
+
upAxis: String.fromCharCode(stageUpAxis),
|
|
377
|
+
startTimeCode: stageStartTimeCode,
|
|
378
|
+
endTimeCode: stageEndTimeCode,
|
|
379
|
+
timeCodesPerSecond: stageTimeCodesPerSecond,
|
|
380
|
+
});
|
|
166
381
|
console.log("VIRTUAL FILESYSTEM", USD.FS_analyzePath("/"));
|
|
167
382
|
}
|
|
168
383
|
|
|
169
384
|
return {
|
|
170
385
|
driver: /** @type {import(".").HdWebSyncDriver} */ (driverOrPromise),
|
|
386
|
+
ready: () => initialDrawPromise,
|
|
171
387
|
update: (dt) => {
|
|
172
388
|
// ensure we're not dead
|
|
173
389
|
if (driver.isDeleted()) {
|
|
@@ -178,22 +394,104 @@ export async function createThreeHydra(config) {
|
|
|
178
394
|
}
|
|
179
395
|
return;
|
|
180
396
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
397
|
+
if (playing) {
|
|
398
|
+
time += dt;
|
|
399
|
+
const startTimeCode = stageStartTimeCode;
|
|
400
|
+
const duration = stageDuration();
|
|
401
|
+
let timecode = startTimeCode + time * stageTimeCodesPerSecond;
|
|
402
|
+
if (duration > 0) {
|
|
403
|
+
timecode = startTimeCode + ((timecode - startTimeCode) % duration);
|
|
404
|
+
currentTimeCode = timecode;
|
|
405
|
+
driver.SetTime(timecode);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
draw();
|
|
409
|
+
},
|
|
410
|
+
setTime: async (timeCode) => {
|
|
411
|
+
if (disposed || driver.isDeleted()) {
|
|
412
|
+
return Promise.resolve();
|
|
413
|
+
}
|
|
414
|
+
await drawPromise;
|
|
415
|
+
setHydraTime(timeCode);
|
|
416
|
+
return draw(true);
|
|
417
|
+
},
|
|
418
|
+
getTime: () => currentTimeCode,
|
|
419
|
+
setPlaying: (value) => {
|
|
420
|
+
playing = Boolean(value);
|
|
421
|
+
},
|
|
422
|
+
isPlaying: () => playing,
|
|
423
|
+
refresh: () => draw(),
|
|
424
|
+
setIncludedPurposes: async (includedPurposes) => {
|
|
425
|
+
if (disposed || driver.isDeleted() || typeof driver.SetIncludedPurposes !== "function") {
|
|
426
|
+
return Promise.resolve();
|
|
427
|
+
}
|
|
428
|
+
await drawPromise;
|
|
429
|
+
driver.SetIncludedPurposes(includedPurposes);
|
|
430
|
+
return draw(true);
|
|
431
|
+
},
|
|
432
|
+
editStage: async (callback) => {
|
|
433
|
+
if (disposed || driver.isDeleted()) {
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
await drawPromise;
|
|
437
|
+
if (drawInFlight) {
|
|
438
|
+
console.warn("Skipping USD stage edit while Hydra draw is still pending.");
|
|
439
|
+
return undefined;
|
|
440
|
+
}
|
|
441
|
+
if (disposed || driver.isDeleted()) {
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
const stage = await waitMaybeAsync(driver.GetStage());
|
|
445
|
+
editInFlight = true;
|
|
446
|
+
try {
|
|
447
|
+
const result = await callback(stage, driver);
|
|
448
|
+
if (disposed || driver.isDeleted()) {
|
|
449
|
+
return result;
|
|
450
|
+
}
|
|
451
|
+
await withTimeout(waitMaybeAsync(driver.Repopulate()), "Hydra repopulate");
|
|
452
|
+
editInFlight = false;
|
|
453
|
+
await draw(true);
|
|
454
|
+
return result;
|
|
455
|
+
}
|
|
456
|
+
finally {
|
|
457
|
+
editInFlight = false;
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
repopulate: async () => {
|
|
461
|
+
if (disposed || driver.isDeleted()) {
|
|
462
|
+
return Promise.resolve();
|
|
463
|
+
}
|
|
464
|
+
await withTimeout(waitMaybeAsync(driver.Repopulate()), "Hydra repopulate");
|
|
465
|
+
return draw();
|
|
466
|
+
},
|
|
467
|
+
materialsReady: () => renderInterface.waitForMaterialsReady(),
|
|
468
|
+
diagnostics: () => renderInterface.getDiagnostics(),
|
|
469
|
+
stageMetadata: () => {
|
|
470
|
+
applyStageMetadata(readStageMetadata());
|
|
471
|
+
return {
|
|
472
|
+
upAxis: String.fromCharCode(stageUpAxis),
|
|
473
|
+
startTimeCode: stageStartTimeCode,
|
|
474
|
+
endTimeCode: stageEndTimeCode,
|
|
475
|
+
timeCodesPerSecond: stageTimeCodesPerSecond,
|
|
476
|
+
};
|
|
186
477
|
},
|
|
187
478
|
/**
|
|
188
|
-
*
|
|
479
|
+
* Dispose the Three Hydra delegate.
|
|
189
480
|
* This does *not* clear the threejs scene but only dispose the USD delegate and loaded files
|
|
190
481
|
*/
|
|
191
|
-
dispose: () => {
|
|
482
|
+
dispose: async () => {
|
|
483
|
+
if (disposed) return;
|
|
484
|
+
disposed = true;
|
|
192
485
|
if (debug) console.warn("Disposing Three Hydra");
|
|
193
486
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
487
|
+
const cleanup = async () => {
|
|
488
|
+
await renderInterface.waitForMaterialsReady().catch(() => {});
|
|
489
|
+
renderInterface.dispose();
|
|
490
|
+
disposeObjectTree(scenePrimitiveRoot);
|
|
491
|
+
|
|
492
|
+
// Unlink all generated files and folders in the virtual file system.
|
|
493
|
+
const unlinkedFiles = new Set();
|
|
494
|
+
function unlinkFiles(dir, path) {
|
|
197
495
|
for (const fileName of Object.keys(dir.contents)) {
|
|
198
496
|
const file = dir.contents[fileName];
|
|
199
497
|
if (file.isFolder) {
|
|
@@ -206,7 +504,7 @@ export async function createThreeHydra(config) {
|
|
|
206
504
|
config.USD.FS_rmdir(path + fileName);
|
|
207
505
|
}
|
|
208
506
|
catch (e) {
|
|
209
|
-
console.
|
|
507
|
+
if (debug) console.debug("Error unlinking folder", fullPath, e);
|
|
210
508
|
}
|
|
211
509
|
}
|
|
212
510
|
else {
|
|
@@ -216,37 +514,48 @@ export async function createThreeHydra(config) {
|
|
|
216
514
|
config.USD.FS_unlink(fullPath);
|
|
217
515
|
}
|
|
218
516
|
catch (e) {
|
|
219
|
-
console.
|
|
517
|
+
if (debug) console.debug("Error unlinking", fullPath, e);
|
|
220
518
|
}
|
|
221
519
|
}
|
|
222
520
|
}
|
|
223
|
-
|
|
521
|
+
}
|
|
224
522
|
|
|
225
|
-
|
|
523
|
+
function rmRootDir(rootDir) {
|
|
226
524
|
const allFiles = config.USD.FS_analyzePath(rootDir).object;
|
|
227
525
|
if (allFiles)
|
|
228
526
|
unlinkFiles(allFiles, rootDir);
|
|
229
|
-
|
|
527
|
+
}
|
|
230
528
|
|
|
231
|
-
|
|
232
|
-
|
|
529
|
+
rmRootDir("/" + directoryForFiles);
|
|
530
|
+
rmRootDir("/1/"); // HTTPAssetResolver puts files into a series of folders named "/1/1/1/1" to allow for parent traversal
|
|
233
531
|
|
|
234
|
-
|
|
532
|
+
if (!unlinkedFiles.has(file)) {
|
|
235
533
|
if (debug) console.warn("Unlinking main file", file);
|
|
236
534
|
let fileToUnlink = file;
|
|
237
535
|
if (fileToUnlink.startsWith("http"))
|
|
238
536
|
fileToUnlink = "/" + fileToUnlink.replace("://", ":/");
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
537
|
+
if (config.USD.FS_analyzePath(fileToUnlink)?.exists) {
|
|
538
|
+
try {
|
|
539
|
+
config.USD.FS_unlink(fileToUnlink);
|
|
540
|
+
} catch (e) {
|
|
541
|
+
if (debug) console.debug("Error unlinking main file", fileToUnlink, e);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
243
544
|
}
|
|
244
|
-
}
|
|
245
545
|
|
|
246
|
-
|
|
247
|
-
|
|
546
|
+
if (!driver.isDeleted()) {
|
|
547
|
+
driver.delete();
|
|
548
|
+
}
|
|
549
|
+
driverOrPromise = null;
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
if (drawInFlight) {
|
|
553
|
+
console.warn("Waiting for pending Hydra draw before USD cleanup.");
|
|
554
|
+
}
|
|
555
|
+
await drawPromise.catch(() => {});
|
|
556
|
+
await cleanup();
|
|
248
557
|
|
|
249
558
|
if (debug) console.warn("Disposed Three Hydra");
|
|
250
559
|
},
|
|
251
560
|
}
|
|
252
|
-
}
|
|
561
|
+
}
|