@needle-tools/usd 0.0.1-1d6bd1e

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 (47) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +100 -0
  3. package/examples/index.html +58 -0
  4. package/examples/package-lock.json +1548 -0
  5. package/examples/package.json +24 -0
  6. package/examples/public/HttpReferences copy.usda +46 -0
  7. package/examples/public/HttpReferences.usda +44 -0
  8. package/examples/public/gingerbread/GingerbreadHouse.usda +35 -0
  9. package/examples/public/gingerbread/house/GingerBreadHouse.usdc +0 -0
  10. package/examples/public/gingerbread/house/textures/color.jpg +0 -0
  11. package/examples/public/gingerbread/house/textures/metallic_roughness.jpg +0 -0
  12. package/examples/public/gingerbread/house/textures/normal.jpg +0 -0
  13. package/examples/public/gingerbread/snowman/Snowman.usdc +0 -0
  14. package/examples/public/gingerbread/snowman/textures/color.jpg +0 -0
  15. package/examples/public/gingerbread/snowman/textures/metallic_roughness.jpg +0 -0
  16. package/examples/public/gingerbread/snowman/textures/normal.jpg +0 -0
  17. package/examples/public/test.usdz +0 -0
  18. package/examples/public/vite.svg +1 -0
  19. package/examples/src/fileHandling.ts +256 -0
  20. package/examples/src/main.ts +167 -0
  21. package/examples/src/three.ts +140 -0
  22. package/examples/src/vite-env.d.ts +1 -0
  23. package/examples/tsconfig.json +23 -0
  24. package/examples/vite.config.js +21 -0
  25. package/package.json +50 -0
  26. package/src/bindings/.gitattributes +2 -0
  27. package/src/bindings/emHdBindings.data +19331 -0
  28. package/src/bindings/emHdBindings.js +12227 -0
  29. package/src/bindings/emHdBindings.wasm +0 -0
  30. package/src/bindings/emHdBindings.worker.js +124 -0
  31. package/src/bindings/index.js +124 -0
  32. package/src/create.three.js +252 -0
  33. package/src/hydra/ThreeJsRenderDelegate.js +872 -0
  34. package/src/hydra/consoleRenderDelegate.js +47 -0
  35. package/src/hydra/index.js +5 -0
  36. package/src/index.d.ts +1 -0
  37. package/src/index.js +5 -0
  38. package/src/plugins/index.js +2 -0
  39. package/src/plugins/needle.js +158 -0
  40. package/src/types/bindings.d.ts +82 -0
  41. package/src/types/create.three.d.ts +65 -0
  42. package/src/types/hydra.d.ts +32 -0
  43. package/src/types/index.d.ts +5 -0
  44. package/src/types/plugins.d.ts +9 -0
  45. package/src/types/vite.d.ts +19 -0
  46. package/src/utils.js +24 -0
  47. package/src/vite/index.js +22 -0
Binary file
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var Module = {};
3
+ var ENVIRONMENT_IS_NODE =
4
+ typeof process == "object" &&
5
+ typeof process.versions == "object" &&
6
+ typeof process.versions.node == "string";
7
+ if (ENVIRONMENT_IS_NODE) {
8
+ var nodeWorkerThreads = require("worker_threads");
9
+ var parentPort = nodeWorkerThreads.parentPort;
10
+ parentPort.on("message", (data) => onmessage({ data: data }));
11
+ var fs = require("fs");
12
+ Object.assign(global, {
13
+ self: global,
14
+ require: require,
15
+ Module: Module,
16
+ location: { href: __filename },
17
+ Worker: nodeWorkerThreads.Worker,
18
+ importScripts: (f) =>
19
+ (0, eval)(fs.readFileSync(f, "utf8") + "//# sourceURL=" + f),
20
+ postMessage: (msg) => parentPort.postMessage(msg),
21
+ performance: global.performance || { now: Date.now },
22
+ });
23
+ }
24
+ var initializedJS = false;
25
+ function threadPrintErr() {
26
+ var text = Array.prototype.slice.call(arguments).join(" ");
27
+ if (ENVIRONMENT_IS_NODE) {
28
+ fs.writeSync(2, text + "\n");
29
+ return;
30
+ }
31
+ console.error(text);
32
+ }
33
+ function threadAlert() {
34
+ var text = Array.prototype.slice.call(arguments).join(" ");
35
+ postMessage({
36
+ cmd: "alert",
37
+ text: text,
38
+ threadId: Module["_pthread_self"](),
39
+ });
40
+ }
41
+ var err = threadPrintErr;
42
+ self.alert = threadAlert;
43
+ Module["instantiateWasm"] = (info, receiveInstance) => {
44
+ var module = Module["wasmModule"];
45
+ Module["wasmModule"] = null;
46
+ var instance = new WebAssembly.Instance(module, info);
47
+ return receiveInstance(instance);
48
+ };
49
+ self.onunhandledrejection = (e) => {
50
+ throw e.reason || e;
51
+ };
52
+ function handleMessage(e) {
53
+ try {
54
+ if (e.data.cmd === "load") {
55
+ let messageQueue = [];
56
+ self.onmessage = (e) => messageQueue.push(e);
57
+ // self.addEventListener("message", e => console.log("\x1B[1;mworker received message: " + e.data.cmd + "\x1B[0m"));
58
+ self.startWorker = (instance) => {
59
+ Module = instance;
60
+ postMessage({ cmd: "loaded" });
61
+ for (let msg of messageQueue) {
62
+ handleMessage(msg);
63
+ }
64
+ self.onmessage = handleMessage;
65
+ };
66
+ Module["wasmModule"] = e.data.wasmModule;
67
+ for (const handler of e.data.handlers) {
68
+ Module[handler] = (...args) => {
69
+ postMessage({ cmd: "callHandler", handler: handler, args: args });
70
+ };
71
+ }
72
+ Module["wasmMemory"] = e.data.wasmMemory;
73
+ Module["buffer"] = Module["wasmMemory"].buffer;
74
+ Module["ENVIRONMENT_IS_PTHREAD"] = true;
75
+ if (typeof e.data.urlOrBlob == "string") {
76
+ importScripts(e.data.urlOrBlob);
77
+ } else {
78
+ var objectUrl = URL.createObjectURL(e.data.urlOrBlob);
79
+ importScripts(objectUrl);
80
+ URL.revokeObjectURL(objectUrl);
81
+ }
82
+ getUsdModule(Module);
83
+ } else if (e.data.cmd === "run") {
84
+ Module["__emscripten_thread_init"](e.data.pthread_ptr, 0, 0, 1);
85
+ Module["__emscripten_thread_mailbox_await"](e.data.pthread_ptr);
86
+ Module["establishStackSpace"]();
87
+ Module["PThread"].receiveObjectTransfer(e.data);
88
+ Module["PThread"].threadInitTLS();
89
+ if (!initializedJS) {
90
+ Module["__embind_initialize_bindings"]();
91
+ initializedJS = true;
92
+ }
93
+ try {
94
+ Module["invokeEntryPoint"](e.data.start_routine, e.data.arg);
95
+ } catch (ex) {
96
+ if (ex != "unwind") {
97
+ throw ex;
98
+ }
99
+ }
100
+ } else if (e.data.cmd === "cancel") {
101
+ if (Module["_pthread_self"]()) {
102
+ Module["__emscripten_thread_exit"](-1);
103
+ }
104
+ } else if (e.data.target === "setimmediate") {
105
+ } else if (e.data.cmd === "checkMailbox") {
106
+ if (initializedJS) {
107
+ Module["checkMailbox"]();
108
+ }
109
+ }
110
+ else if (e.data.cmd === "callHandlerAsyncResult") {
111
+ // ignore, handled elsewhere
112
+ }
113
+ else if (e.data.cmd) {
114
+ err(`worker.js received unknown command ${e.data.cmd}`);
115
+ err(e.data);
116
+ }
117
+ } catch (ex) {
118
+ if (Module["__emscripten_thread_crashed"]) {
119
+ Module["__emscripten_thread_crashed"]();
120
+ }
121
+ throw ex;
122
+ }
123
+ }
124
+ self.onmessage = handleMessage;
@@ -0,0 +1,124 @@
1
+ import "./emHdBindings.js";
2
+
3
+ /**
4
+ * @type {Promise<import("..").USD> | null}
5
+ */
6
+ let usd_module_promise = null;
7
+
8
+
9
+ /**
10
+ * @param {undefined | import("..").GetUsdModuleOptions} opts
11
+ */
12
+ export async function getUsdModule(opts) {
13
+
14
+ if (usd_module_promise) {
15
+ return usd_module_promise;
16
+ }
17
+
18
+
19
+ /**
20
+ * @type {import("..").getUsdModule}
21
+ */
22
+ const getUsdModuleFn = globalThis["NEEDLE:USD:GET"];
23
+
24
+ if (!getUsdModuleFn) {
25
+ throw new Error("\"NEEDLE:USD:GET\" not found in globalThis - please modify \"emHdBindings.js\" and add: globalThis[\"NEEDLE:USD:GET\"] = getUsdModule;");
26
+ }
27
+
28
+
29
+ // HACK for worker import: \"Cannot use import statement outside a module\""
30
+ // https://github.com/vitejs/vite/issues/6979
31
+
32
+ // @ts-ignore
33
+ const isProd = import.meta.env?.PROD ?? true;
34
+
35
+
36
+ /**
37
+ * We use a async import here because otherwise sveltekit vite complains about unknown file extensions (e.g. .wasm)
38
+ */
39
+ const bindingsPromise = await Promise.all([
40
+ /** @ts-ignore */
41
+ import(`./emHdBindings.js?url`),
42
+ /** @ts-ignore */
43
+ import(`./emHdBindings.data?url`),
44
+ // https://v3.vitejs.dev/guide/features.html#web-workers
45
+ // https://github.com/vitejs/vite/issues/6979
46
+ /** @ts-ignore */
47
+ import(`./emHdBindings.worker.js?worker&url`),
48
+ /** @ts-ignore */
49
+ import(`./emHdBindings.worker.js?url`),
50
+ /** @ts-ignore */
51
+ import(`./emHdBindings.wasm?url`),
52
+ ]);
53
+ const [bindings, data, workerProd, workerDev, wasm] = bindingsPromise;
54
+ const worker = isProd ? workerProd : workerDev;
55
+ const preloaded_data = await fetch(data.default).then(r => r.arrayBuffer());
56
+
57
+ return usd_module_promise = getUsdModuleFn({
58
+ mainScriptUrlOrBlob: bindings.default,// "./emHdBindings.js",
59
+ ...opts,
60
+ setStatus: (status) => {
61
+ // TODO: would be nice to have a progress event
62
+ // for now we parse the log 'Downloading data... (219516/849069)'
63
+ if (opts?.onDownloadProgress && status.includes("Downloading data...")) {
64
+ const start = status.indexOf("(");
65
+ const end = status.indexOf("/");
66
+ const start2 = status.indexOf("/", end);
67
+ const end2 = status.indexOf(")", start2);
68
+ const startNum = parseInt(status.substring(start + 1, end));
69
+ const endNum = parseInt(status.substring(end + 1, end2));
70
+ opts.onDownloadProgress(startNum, endNum);
71
+ }
72
+ if (opts?.setStatus) opts.setStatus(status);
73
+ else console.debug("🧊 USD STATUS", status);
74
+ },
75
+ locateFile: (file) => {
76
+ // if (opts?.debug === true) console.warn("LOCATE FILE:", file)
77
+
78
+ const userResult = opts?.locateFile?.(file);
79
+ if (userResult) {
80
+ return userResult;
81
+ }
82
+
83
+ /** resolved filepath */
84
+ let res = null;
85
+
86
+ if (file.includes("emHdBindings.data")) {
87
+ res = data.default;
88
+ }
89
+ else if (file.includes("emHdBindings.wasm")) {
90
+ res = wasm.default;
91
+ }
92
+ else if (file.includes("emHdBindings.worker.js")) {
93
+ res = worker.default;
94
+ }
95
+
96
+ // if (url?.startsWith("data:text/javascript;base64")) {
97
+ // // we're client side and Buffer and atob are not available
98
+ // // so we need to convert the base64 to a blob
99
+ // const base64 = url.split(",")[1];
100
+ // const binary = atob(base64);
101
+ // const bytes = new Uint8Array(binary.length);
102
+ // for (let i = 0; i < binary.length; i++) {
103
+ // bytes[i] = binary.charCodeAt(i);
104
+ // }
105
+
106
+ // }
107
+
108
+ return res ?? file;
109
+ },
110
+ getPreloadedPackage(name, size) {
111
+ const userResult = opts?.getPreloadedPackage?.(name, size);
112
+ if (userResult) return userResult;
113
+
114
+ // For debugging if the data file isnt loaded or the size might be wrong
115
+ // Make sure to clear the vite cache. See https://linear.app/needle/issue/NE-4851#comment-2a9538e3
116
+ if (name.includes("emHdBindings.data")) {
117
+ if (preloaded_data.byteLength !== size) {
118
+ throw new Error(`emHdBindings.data size mismatch: expected ${size} but got ${preloaded_data.byteLength}\n${data.default}`);
119
+ }
120
+ }
121
+ return null;
122
+ },
123
+ });
124
+ }
@@ -0,0 +1,252 @@
1
+ import { threeJsRenderDelegate } from "./hydra/index.js";
2
+ import { tryDetermineFileFormat } from "./utils.js";
3
+
4
+
5
+ /**
6
+ * @param {{USD:import("./types").USD, filepath:string, buffer?:ArrayBuffer, parent?:string,}} opts
7
+ */
8
+ async function createFile(opts) {
9
+ if (typeof opts.filepath !== "string") throw new Error("Filepath must be a string");
10
+
11
+ let filepath = /** @type {string} */ (opts.filepath);
12
+
13
+ let arrayBuffer = opts.buffer;
14
+ if (!arrayBuffer) {
15
+ const blob = await fetch(filepath);
16
+ arrayBuffer = await blob.arrayBuffer();
17
+ }
18
+
19
+ // ensure that file paths are not using slashes
20
+ /** @ts-ignore */
21
+ filepath = filepath.replaceAll(/\\/g, "/").replaceAll("/", "_");
22
+
23
+ const format = tryDetermineFileFormat(arrayBuffer);
24
+ const ext = filepath.split(".").pop();
25
+ if (ext !== "usdz" && ext !== "usd" && ext !== "usda") {
26
+ if (format === "usdz" || format === "usd" || format === "usda") {
27
+ filepath += "." + format;
28
+ } else {
29
+ console.warn("Unknown file format - assuming .usdz");
30
+ filepath += ".usdz";
31
+ }
32
+ }
33
+ else if (format !== ext) {
34
+ console.warn("File extension does not match file format", { ext, format });
35
+ }
36
+
37
+ // Put a simple USDZ file into the virtual file system so USD can access it
38
+ // Create a file in the virtual file system
39
+ opts.USD.FS_createDataFile("", filepath, new Uint8Array(arrayBuffer), true, true, true);
40
+ return filepath;
41
+ }
42
+
43
+ export class USDLoadingManager {
44
+ static urlModifier = null;
45
+ static setURLModifier(urlModifier) {
46
+ USDLoadingManager.urlModifier = urlModifier;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Set up a Three.js Hydra render delegate.
52
+ * @param {import(".").createThreeHydraConfig} config
53
+ * @returns {Promise<import(".").NeedleThreeHydraHandle>}
54
+ */
55
+ export async function createThreeHydra(config) {
56
+ const debug = config.debug || false;
57
+
58
+ config.USD.debug = debug;
59
+
60
+ if (debug) console.log("USD", config.USD);
61
+
62
+ await config.USD.ready.catch(console.error);
63
+
64
+ const { buffer, USD } = config;
65
+
66
+ // Some common directory is needed so that we don't get clashes with root-level files
67
+ // and directories in the virtual file system
68
+ const directoryForFiles = "needle/";
69
+
70
+ // We're loading all provided files into the virtual file system.
71
+ // Potentially, we could also resolve dropped files on the fly and load them only when needed,
72
+ // but this requires HTTPAssetResolver changes
73
+ if (Array.isArray(config.files)) {
74
+ for (const file of config.files) {
75
+ let fileName = file.name;
76
+ let directory = "/";
77
+ if (file.path) {
78
+ const parts = file.path.split('/');
79
+ if (parts.length > 1) {
80
+ fileName = parts.pop() || fileName;
81
+ directory = file.path.substring(0, file.path.length - fileName.length);
82
+ }
83
+ }
84
+
85
+ USD.FS_createPath("", directoryForFiles + directory, true, true);
86
+ USD.FS_createDataFile(directoryForFiles + directory, fileName, new Uint8Array(await file.arrayBuffer()), true, true, true);
87
+ }
88
+ }
89
+
90
+ // Capabilities of the loader.
91
+ // This depends on the HttpAssetResolver implementation and the virtual file system.
92
+ const allowFetchWebUrls = true;
93
+ const allowFetchLocalFiles = true;
94
+
95
+ // Which file we actually load as root file depends:
96
+ // - when an array of files is provided, we use the first one;
97
+ // - when a URL is provided, we use that;
98
+ // - when a blob is provided, we create a file from that blob and sanitize the filename.
99
+ let file = "";
100
+ if (config.files?.length) {
101
+ file = directoryForFiles + config.files[0].path;
102
+ }
103
+ 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;
111
+ }
112
+ else {
113
+ file = await createFile({ USD, filepath: config.url, buffer });
114
+ }
115
+ }
116
+
117
+ // Log the virtual file system, composed of a hierarchy of FSNode objects
118
+ if (debug) {
119
+ console.log("VIRTUAL FILESYSTEM", USD.FS_analyzePath("/"));
120
+ console.log("MAIN FILE", file);
121
+ }
122
+
123
+ /**
124
+ * @type {null | import(".").HdWebSyncDriver | Promise<import(".").HdWebSyncDriver>}
125
+ */
126
+ let driverOrPromise = null;
127
+
128
+ /**
129
+ * @type {import(".").threeJsRenderDelegateConfig}
130
+ */
131
+ const delegateConfig = {
132
+ usdRoot: config.scene,
133
+ paths: new Array(),
134
+ driver: () => /** @type {import(".").HdWebSyncDriver} */(driverOrPromise),
135
+ };
136
+
137
+ const renderInterface = new threeJsRenderDelegate(delegateConfig);
138
+
139
+ if (debug) console.log("RENDER INTERFACE", renderInterface);
140
+
141
+ driverOrPromise = new config.USD.HdWebSyncDriver(renderInterface, file);
142
+ if (driverOrPromise instanceof Promise) {
143
+ driverOrPromise = await driverOrPromise;
144
+ }
145
+
146
+ const driver = /** @type {import(".").HdWebSyncDriver} */ (driverOrPromise);
147
+
148
+ if (debug) console.log("DRIVER", driver);
149
+
150
+ /** Draw once */
151
+ driver.Draw();
152
+
153
+ let stage = driver.GetStage();
154
+ if (stage instanceof Promise) {
155
+ stage = await stage;
156
+ stage = driver.GetStage();
157
+ }
158
+
159
+ /** 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;
161
+
162
+ let time = 0;
163
+
164
+ if (debug) {
165
+ console.log("STAGE", stage);
166
+ console.log("VIRTUAL FILESYSTEM", USD.FS_analyzePath("/"));
167
+ }
168
+
169
+ return {
170
+ driver: /** @type {import(".").HdWebSyncDriver} */ (driverOrPromise),
171
+ update: (dt) => {
172
+ // ensure we're not dead
173
+ if (driver.isDeleted()) {
174
+ if (debug) {
175
+ if (config["debug:delete"] === undefined)
176
+ console.error("Called update for three hydra after it was deleted!");
177
+ config["debug:delete"] = true;
178
+ }
179
+ return;
180
+ }
181
+ time += dt;
182
+ let timecode = time * stage.GetTimeCodesPerSecond();
183
+ timecode = timecode % (stage.GetEndTimeCode() - stage.GetStartTimeCode());
184
+ driver.SetTime(timecode);
185
+ driver.Draw();
186
+ },
187
+ /**
188
+ * Dipoose the Three Hydra delegate.
189
+ * This does *not* clear the threejs scene but only dispose the USD delegate and loaded files
190
+ */
191
+ dispose: () => {
192
+ if (debug) console.warn("Disposing Three Hydra");
193
+
194
+ // Unlink all generated files and folders in the virtual file system.
195
+ const unlinkedFiles = new Set();
196
+ function unlinkFiles(dir, path) {
197
+ for (const fileName of Object.keys(dir.contents)) {
198
+ const file = dir.contents[fileName];
199
+ if (file.isFolder) {
200
+ unlinkFiles(file, path + fileName + "/");
201
+ }
202
+ const fullPath = path + fileName;
203
+ if (file.isFolder) {
204
+ if (debug) console.log("unlinking folder", fullPath);
205
+ try {
206
+ config.USD.FS_rmdir(path + fileName);
207
+ }
208
+ catch (e) {
209
+ console.error("Error unlinking folder", fullPath, e);
210
+ }
211
+ }
212
+ else {
213
+ if (debug) console.log("unlinking", fullPath);
214
+ unlinkedFiles.add(fullPath);
215
+ try {
216
+ config.USD.FS_unlink(fullPath);
217
+ }
218
+ catch (e) {
219
+ console.error("Error unlinking", fullPath, e);
220
+ }
221
+ }
222
+ }
223
+ }
224
+
225
+ function rmRootDir(rootDir) {
226
+ const allFiles = config.USD.FS_analyzePath(rootDir).object;
227
+ if (allFiles)
228
+ unlinkFiles(allFiles, rootDir);
229
+ }
230
+
231
+ rmRootDir("/" + directoryForFiles);
232
+ rmRootDir("/1/"); // HTTPAssetResolver puts files into a series of folders named "/1/1/1/1" to allow for parent traversal
233
+
234
+ if (!unlinkedFiles.has(file)) {
235
+ if (debug) console.warn("Unlinking main file", file);
236
+ let fileToUnlink = file;
237
+ if (fileToUnlink.startsWith("http"))
238
+ fileToUnlink = "/" + fileToUnlink.replace("://", ":/");
239
+ try {
240
+ config.USD.FS_unlink(fileToUnlink);
241
+ } catch (e) {
242
+ console.error("Error unlinking main file", fileToUnlink, e);
243
+ }
244
+ }
245
+
246
+ driver.delete();
247
+ driverOrPromise = null;
248
+
249
+ if (debug) console.warn("Disposed Three Hydra");
250
+ },
251
+ }
252
+ }