@needle-tools/engine 5.1.0-alpha.7 → 5.1.0-alpha.9
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 +17 -0
- package/components.needle.json +1 -1
- package/dist/{materialx-CE2sUv2B.umd.cjs → materialx-B47Bz-xs.umd.cjs} +1 -1
- package/dist/{materialx-u1EqYrhu.min.js → materialx-B85WjP7E.min.js} +1 -1
- package/dist/{materialx-vyB2Zbt4.js → materialx-jkHmVPez.js} +1 -1
- package/dist/{needle-engine.bundle-BNj2FJS7.min.js → needle-engine.bundle-BotdnwVF.min.js} +173 -173
- package/dist/{needle-engine.bundle-tHGdwr9a.umd.cjs → needle-engine.bundle-DagaNrZK.umd.cjs} +147 -147
- package/dist/{needle-engine.bundle-BwS5IsLn.js → needle-engine.bundle-mtqHZ45d.js} +7832 -7624
- package/dist/needle-engine.d.ts +301 -23
- package/dist/needle-engine.js +575 -565
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/dist/{vendor-ButPLzor.umd.cjs → vendor-1UvpPPSB.umd.cjs} +2 -2
- package/dist/{vendor-DqZC4Is7.min.js → vendor-BQ2Vuntm.min.js} +4 -4
- package/dist/{vendor-DkWSNjMV.js → vendor-BslSKZPo.js} +113 -106
- package/lib/engine/api.d.ts +1 -1
- package/lib/engine/api.js +1 -1
- package/lib/engine/api.js.map +1 -1
- package/lib/engine/engine_audio.d.ts +30 -1
- package/lib/engine/engine_audio.js +57 -13
- package/lib/engine/engine_audio.js.map +1 -1
- package/lib/engine/engine_init.js +2 -2
- package/lib/engine/engine_init.js.map +1 -1
- package/lib/engine/engine_instantiate_resolve.js +40 -10
- package/lib/engine/engine_instantiate_resolve.js.map +1 -1
- package/lib/engine/engine_license.d.ts +7 -7
- package/lib/engine/engine_license.js +72 -72
- package/lib/engine/engine_license.js.map +1 -1
- package/lib/engine/engine_math.d.ts +1 -1
- package/lib/engine/engine_math.js +10 -4
- package/lib/engine/engine_math.js.map +1 -1
- package/lib/engine/engine_networking_blob.js +3 -3
- package/lib/engine/engine_networking_blob.js.map +1 -1
- package/lib/engine/engine_scenedata.js +7 -0
- package/lib/engine/engine_scenedata.js.map +1 -1
- package/lib/engine/engine_serialization_core.js +7 -2
- package/lib/engine/engine_serialization_core.js.map +1 -1
- package/lib/engine/engine_time_utils.d.ts +10 -6
- package/lib/engine/engine_time_utils.js +10 -6
- package/lib/engine/engine_time_utils.js.map +1 -1
- package/lib/engine/engine_utils_qrcode.js +2 -2
- package/lib/engine/engine_utils_qrcode.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu.js +5 -5
- package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.js +2 -2
- package/lib/engine/webcomponents/needle-engine.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.loading.js +2 -2
- package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
- package/lib/engine/xr/TempXRContext.js +2 -2
- package/lib/engine/xr/TempXRContext.js.map +1 -1
- package/lib/engine-components/AudioSource.d.ts +46 -2
- package/lib/engine-components/AudioSource.js +268 -60
- package/lib/engine-components/AudioSource.js.map +1 -1
- package/lib/engine-components/DragControls.js +2 -2
- package/lib/engine-components/DragControls.js.map +1 -1
- package/lib/engine-components/DragControlsConstraints.d.ts +0 -15
- package/lib/engine-components/DragControlsConstraints.js +14 -8
- package/lib/engine-components/DragControlsConstraints.js.map +1 -1
- package/lib/engine-components/EventList.js +1 -1
- package/lib/engine-components/EventList.js.map +1 -1
- package/lib/engine-components/SceneSwitcher.js +17 -2
- package/lib/engine-components/SceneSwitcher.js.map +1 -1
- package/lib/engine-components/codegen/components.d.ts +10 -0
- package/lib/engine-components/codegen/components.js +10 -0
- package/lib/engine-components/codegen/components.js.map +1 -1
- package/lib/engine-components/export/usdz/USDZExporter.js +4 -4
- package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
- package/lib/engine-components/timeline/PlayableDirector.d.ts +6 -0
- package/lib/engine-components/timeline/PlayableDirector.js +25 -12
- package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
- package/lib/engine-components/web/ScrollFollow.d.ts +33 -0
- package/lib/engine-components/web/ScrollFollow.js +164 -22
- package/lib/engine-components/web/ScrollFollow.js.map +1 -1
- package/lib/needle-app.d.ts +18 -0
- package/lib/needle-app.js +19 -0
- package/lib/needle-app.js.map +1 -0
- package/package.json +3 -2
- package/plugins/common/license.js +25 -4
- package/plugins/common/needle-engine.js +5 -2
- package/plugins/types/userconfig.d.ts +22 -0
- package/plugins/vite/dts-generator.d.ts +24 -0
- package/plugins/vite/dts-generator.js +40 -7
- package/plugins/vite/index.d.ts +2 -0
- package/plugins/vite/index.js +17 -0
- package/plugins/vite/license.js +4 -4
- package/plugins/vite/needle-app.js +78 -5
- package/plugins/vite/remote.d.ts +28 -0
- package/plugins/vite/remote.js +124 -0
- package/src/engine/api.ts +1 -1
- package/src/engine/engine_audio.ts +68 -15
- package/src/engine/engine_init.ts +2 -2
- package/src/engine/engine_instantiate_resolve.ts +37 -10
- package/src/engine/engine_license.ts +69 -69
- package/src/engine/engine_math.ts +11 -4
- package/src/engine/engine_networking_blob.ts +3 -3
- package/src/engine/engine_scenedata.ts +7 -2
- package/src/engine/engine_serialization_core.ts +7 -2
- package/src/engine/engine_time_utils.ts +10 -6
- package/src/engine/engine_utils_qrcode.ts +2 -2
- package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
- package/src/engine/webcomponents/needle menu/needle-menu.ts +5 -5
- package/src/engine/webcomponents/needle-engine.loading.ts +6 -6
- package/src/engine/webcomponents/needle-engine.ts +2 -2
- package/src/engine/xr/TempXRContext.ts +2 -2
- package/src/engine-components/AudioSource.ts +256 -55
- package/src/engine-components/DragControls.ts +3 -2
- package/src/engine-components/DragControlsConstraints.ts +16 -8
- package/src/engine-components/EventList.ts +1 -1
- package/src/engine-components/SceneSwitcher.ts +14 -2
- package/src/engine-components/codegen/components.ts +10 -0
- package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
- package/src/engine-components/timeline/PlayableDirector.ts +23 -12
- package/src/engine-components/web/ScrollFollow.ts +168 -22
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* needleDtsGenerator(command, needleConfig, userSettings)
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { join, resolve, dirname } from 'path';
|
|
19
|
+
import { join, resolve, dirname, relative } from 'path';
|
|
20
20
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, realpathSync } from 'fs';
|
|
21
21
|
import { tryLoadProjectConfig } from './config.js';
|
|
22
22
|
import { generateBindingsDts } from '../dts-generator/index.js';
|
|
@@ -35,6 +35,40 @@ try {
|
|
|
35
35
|
// will silently skip generation rather than crashing the dev server.
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Pure helper: compute the desired `html.customData` array for a project.
|
|
40
|
+
*
|
|
41
|
+
* Returns `null` when no change is required (the existing array already matches),
|
|
42
|
+
* otherwise the new array that should be written.
|
|
43
|
+
*
|
|
44
|
+
* The produced entry is a project-relative path with forward slashes. We use
|
|
45
|
+
* `path.relative` rather than a string replace because Vite normalises projectRoot
|
|
46
|
+
* to forward slashes while `htmlDataPath` carries the platform separator from
|
|
47
|
+
* `path.join` — a naive replace fails to match on Windows and leaves an absolute path.
|
|
48
|
+
* Any pre-existing entry pointing at a `needle-html-data.json` (e.g. an absolute path
|
|
49
|
+
* written by an older version of this plugin) is dropped so we don't accumulate
|
|
50
|
+
* duplicates or leave stale absolute paths behind.
|
|
51
|
+
*
|
|
52
|
+
* Exported for testing. The `relativeFn` seam lets tests reproduce platform-specific
|
|
53
|
+
* separator behaviour (`path.win32.relative` / `path.posix.relative`) deterministically.
|
|
54
|
+
*
|
|
55
|
+
* @param {unknown[]} existing Current html.customData entries
|
|
56
|
+
* @param {string} projectRoot
|
|
57
|
+
* @param {string} htmlDataPath Absolute path to the generated needle-html-data.json
|
|
58
|
+
* @param {(from: string, to: string) => string} [relativeFn]
|
|
59
|
+
* @returns {string[] | null}
|
|
60
|
+
*/
|
|
61
|
+
export function computeHtmlCustomData(existing, projectRoot, htmlDataPath, relativeFn = relative) {
|
|
62
|
+
const relPath = relativeFn(projectRoot, htmlDataPath).replace(/\\/g, "/");
|
|
63
|
+
|
|
64
|
+
const cleaned = /** @type {string[]} */ (existing.filter(entry =>
|
|
65
|
+
typeof entry !== "string" || !entry.replace(/\\/g, "/").endsWith("needle-html-data.json")));
|
|
66
|
+
const next = [...cleaned, relPath];
|
|
67
|
+
|
|
68
|
+
if (existing.length === next.length && existing.every((v, i) => v === next[i])) return null;
|
|
69
|
+
return next;
|
|
70
|
+
}
|
|
71
|
+
|
|
38
72
|
/**
|
|
39
73
|
* Ensure `.vscode/settings.json` references the generated `needle-html-data.json`
|
|
40
74
|
* so VS Code provides `data-bind-needle` completions in HTML files automatically.
|
|
@@ -48,9 +82,6 @@ function ensureVscodeHtmlCustomData(projectRoot, htmlDataPath) {
|
|
|
48
82
|
const vscodeDir = join(projectRoot, ".vscode");
|
|
49
83
|
const settingsPath = join(vscodeDir, "settings.json");
|
|
50
84
|
|
|
51
|
-
// Relative path from project root for portability
|
|
52
|
-
const relPath = htmlDataPath.replace(projectRoot + "/", "").replace(projectRoot + "\\", "");
|
|
53
|
-
|
|
54
85
|
/** @type {Record<string, unknown>} */
|
|
55
86
|
let settings = {};
|
|
56
87
|
if (existsSync(settingsPath)) {
|
|
@@ -60,10 +91,12 @@ function ensureVscodeHtmlCustomData(projectRoot, htmlDataPath) {
|
|
|
60
91
|
}
|
|
61
92
|
|
|
62
93
|
const key = "html.customData";
|
|
63
|
-
const existing = Array.isArray(settings[key]) ? /** @type {
|
|
64
|
-
|
|
94
|
+
const existing = Array.isArray(settings[key]) ? /** @type {unknown[]} */ (settings[key]) : [];
|
|
95
|
+
|
|
96
|
+
const next = computeHtmlCustomData(existing, projectRoot, htmlDataPath);
|
|
97
|
+
if (next === null) return; // already up-to-date
|
|
65
98
|
|
|
66
|
-
settings[key] =
|
|
99
|
+
settings[key] = next;
|
|
67
100
|
|
|
68
101
|
mkdirSync(vscodeDir, { recursive: true });
|
|
69
102
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
package/plugins/vite/index.d.ts
CHANGED
|
@@ -56,7 +56,9 @@ export { needleFacebookInstantGames } from "./facebook-instant-games.js";
|
|
|
56
56
|
export { needleImportsLogger } from "./imports-logger.js";
|
|
57
57
|
export { needleBuildInfo } from "./buildinfo.js";
|
|
58
58
|
export { needleApp } from "./needle-app.js";
|
|
59
|
+
export { needleLogger } from "./logger.js";
|
|
59
60
|
export { needleServer } from "./server.js";
|
|
60
61
|
export * from "./gzip.js";
|
|
61
62
|
export * from "./config.js";
|
|
62
63
|
export { needleMakeFilesLocal, needleLocalFilesSceneAnalysis } from "./local-files.js";
|
|
64
|
+
export { needleRemote, isRemoteEnabled } from "./remote.js";
|
package/plugins/vite/index.js
CHANGED
|
@@ -82,6 +82,10 @@ import { needleServer } from "./server.js";
|
|
|
82
82
|
import { needleNPM } from "./npm.js";
|
|
83
83
|
import { needleTransformCode } from "./transform.js";
|
|
84
84
|
import { needleLogger } from "./logger.js";
|
|
85
|
+
export { needleLogger } from "./logger.js";
|
|
86
|
+
import { needleRemote, isRemoteEnabled } from "./remote.js";
|
|
87
|
+
export { needleRemote, isRemoteEnabled } from "./remote.js";
|
|
88
|
+
import { needleLog } from "./logging.js";
|
|
85
89
|
import { viteFixWorkerImport } from "../common/worker.js";
|
|
86
90
|
|
|
87
91
|
export { needleServer } from "./server.js";
|
|
@@ -163,11 +167,24 @@ export async function needlePlugins(command = undefined, config = undefined, use
|
|
|
163
167
|
needleServer(command, config, userSettings),
|
|
164
168
|
needleNPM(command, config, userSettings),
|
|
165
169
|
needleApp(command, config, userSettings),
|
|
170
|
+
needleRemote(command, config, userSettings),
|
|
166
171
|
viteFixWorkerImport(),
|
|
167
172
|
// IMPORTANT: needleBuildInfo should be the last plugin, since it waits for the build pipeline to finish and then collects information about the build output directory
|
|
168
173
|
needleBuildInfo(command, config, userSettings),
|
|
169
174
|
];
|
|
170
175
|
|
|
176
|
+
// Remote debugging serves over HTTPS so WebXR / secure‑context features work on
|
|
177
|
+
// the device. HTTPS is provided by @vitejs/plugin-basic-ssl (a peer the user
|
|
178
|
+
// installs when they opt in); load it lazily so it's never required otherwise.
|
|
179
|
+
if (command !== "build" && isRemoteEnabled(userSettings)) {
|
|
180
|
+
try {
|
|
181
|
+
const { default: basicSsl } = await import("@vitejs/plugin-basic-ssl");
|
|
182
|
+
array.push(basicSsl());
|
|
183
|
+
} catch {
|
|
184
|
+
needleLog("needle-remote", "Remote HTTPS needs `@vitejs/plugin-basic-ssl` — install it: npm i -D @vitejs/plugin-basic-ssl", "warn");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
171
188
|
const asap = await needleAsap(command, config, userSettings);
|
|
172
189
|
if(asap) array.push(asap);
|
|
173
190
|
|
package/plugins/vite/license.js
CHANGED
|
@@ -49,12 +49,12 @@ export function needleLicense(command, config, userSettings) {
|
|
|
49
49
|
let modified = false;
|
|
50
50
|
|
|
51
51
|
// Replace license type
|
|
52
|
-
const index = src.indexOf("
|
|
52
|
+
const index = src.indexOf("DUZpOVcM");
|
|
53
53
|
if (index >= 0) {
|
|
54
54
|
const end = src.indexOf(";", index);
|
|
55
55
|
if (end >= 0) {
|
|
56
56
|
const line = src.substring(index, end);
|
|
57
|
-
const replaced = "
|
|
57
|
+
const replaced = "DUZpOVcM = \"" + licenseResult.type + "\"";
|
|
58
58
|
src = src.replace(line, replaced);
|
|
59
59
|
modified = true;
|
|
60
60
|
}
|
|
@@ -62,12 +62,12 @@ export function needleLicense(command, config, userSettings) {
|
|
|
62
62
|
|
|
63
63
|
// Replace license JWT (same pattern)
|
|
64
64
|
if (licenseResult.jwt) {
|
|
65
|
-
const jwtIndex = src.indexOf("
|
|
65
|
+
const jwtIndex = src.indexOf("__Bjqq");
|
|
66
66
|
if (jwtIndex >= 0) {
|
|
67
67
|
const jwtEnd = src.indexOf(";", jwtIndex);
|
|
68
68
|
if (jwtEnd >= 0) {
|
|
69
69
|
const jwtLine = src.substring(jwtIndex, jwtEnd);
|
|
70
|
-
const jwtReplaced = "
|
|
70
|
+
const jwtReplaced = "__Bjqq = \"" + licenseResult.jwt + "\"";
|
|
71
71
|
src = src.replace(jwtLine, jwtReplaced);
|
|
72
72
|
modified = true;
|
|
73
73
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFile } from 'fs';
|
|
2
2
|
import { tryParseNeedleEngineSrcAttributeFromHtml } from '../common/needle-engine.js';
|
|
3
3
|
import { needleLog } from './logging.js';
|
|
4
|
+
import { getPublicIdentifier } from '../common/license.js';
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
|
|
@@ -21,6 +22,22 @@ export function needleApp(command, config, userSettings) {
|
|
|
21
22
|
|
|
22
23
|
let outputDir = "dist";
|
|
23
24
|
|
|
25
|
+
// The engine API is emitted as a dedicated, deterministically-named facade chunk so that the
|
|
26
|
+
// generated needle-app.js can re-export it - giving users a single import path (the same URL they
|
|
27
|
+
// load needle-app.js from also serves the engine API). See buildStart below.
|
|
28
|
+
// Emitted INTO assets/ (alongside the app's other chunks) on purpose: the app entry lives in assets/
|
|
29
|
+
// and then references the facade with a same-directory specifier ("./needle-app.engine.js"). A root-level
|
|
30
|
+
// facade would be referenced as "../needle-app.engine.js", and Vite computes preload-dependency hints with
|
|
31
|
+
// Node's path.relative, which returns backslashes on Windows for cross-directory paths ("..\\needle-app.engine.js")
|
|
32
|
+
// - a broken modulepreload URL. Keeping it in assets/ avoids the "../" entirely.
|
|
33
|
+
const apiFacadeFileName = "assets/needle-app.engine.js";
|
|
34
|
+
let apiFacadeEmitted = false;
|
|
35
|
+
|
|
36
|
+
// Public project identifier, baked as a comment into the generated needle-app.js for traceability.
|
|
37
|
+
// This is the Needle Cloud `public_key` (the same value injected into client code as NEEDLE_PUBLIC_KEY) -
|
|
38
|
+
// it is PUBLIC and safe to embed. It is NOT the secret NEEDLE_CLOUD_TOKEN. Null when no token is configured.
|
|
39
|
+
let publicKey = null;
|
|
40
|
+
|
|
24
41
|
/**
|
|
25
42
|
* @type {import('vite').Plugin}
|
|
26
43
|
*/
|
|
@@ -28,6 +45,35 @@ export function needleApp(command, config, userSettings) {
|
|
|
28
45
|
{
|
|
29
46
|
name: 'needle:app',
|
|
30
47
|
enforce: "post",
|
|
48
|
+
async buildStart() {
|
|
49
|
+
// Emit @needle-tools/engine/needle-app (a thin re-export of the public API) as its own
|
|
50
|
+
// entry chunk with a fixed filename. Built in the same pass as the app, it deduplicates
|
|
51
|
+
// into the same engine chunks - so importing hooks from it drives the SAME running scene.
|
|
52
|
+
try {
|
|
53
|
+
const resolved = await this.resolve('@needle-tools/engine/needle-app');
|
|
54
|
+
if (resolved?.id) {
|
|
55
|
+
this.emitFile({
|
|
56
|
+
type: 'chunk',
|
|
57
|
+
id: resolved.id,
|
|
58
|
+
fileName: apiFacadeFileName,
|
|
59
|
+
// Preserve the full re-exported API surface. Without this, the build tree-shakes the
|
|
60
|
+
// facade down to only the single export the app internally consumes, so a page-side
|
|
61
|
+
// `import { onStart } from ".../needle-app.js"` fails with "does not provide an export".
|
|
62
|
+
preserveSignature: 'allow-extension',
|
|
63
|
+
});
|
|
64
|
+
apiFacadeEmitted = true;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
needleLog("needle-app", "Could not resolve @needle-tools/engine/needle-app - engine hooks (onStart etc.) will not be importable from needle-app.js", "warn", { dimBody: false });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
needleLog("needle-app", "Failed to emit " + apiFacadeFileName + " facade: " + e.message, "warn", { dimBody: false });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Fetch the public project identifier (best-effort; null without a NEEDLE_CLOUD_TOKEN).
|
|
75
|
+
publicKey = await getPublicIdentifier(undefined).catch(() => null);
|
|
76
|
+
},
|
|
31
77
|
configResolved(config) {
|
|
32
78
|
outputDir = config.build.outDir || "dist";
|
|
33
79
|
},
|
|
@@ -52,7 +98,7 @@ export function needleApp(command, config, userSettings) {
|
|
|
52
98
|
|
|
53
99
|
const main_asset = attribute_src?.[0] || imported_glbs?.[0];
|
|
54
100
|
|
|
55
|
-
const webComponent = generateNeedleEmbedWebComponent(path, main_asset);
|
|
101
|
+
const webComponent = generateNeedleEmbedWebComponent(path, main_asset, apiFacadeEmitted ? apiFacadeFileName : null, publicKey);
|
|
56
102
|
await writeFile(`${outputDir}/needle-app.js`, webComponent, (err) => {
|
|
57
103
|
if (err) {
|
|
58
104
|
needleLog("needle-app", "Could not create needle-app.js: " + err.message, "error", { dimBody: false });
|
|
@@ -78,9 +124,11 @@ export function needleApp(command, config, userSettings) {
|
|
|
78
124
|
/**
|
|
79
125
|
* @param {string} filepath
|
|
80
126
|
* @param {string | null} src
|
|
127
|
+
* @param {string | null | undefined} apiFacadeFile Filename of the emitted engine-API facade chunk (e.g. "needle-app.engine.js"). When set, its API is re-exported so engine hooks can be imported from needle-app.js.
|
|
128
|
+
* @param {string | null | undefined} publicKey Public Needle Cloud project identifier, baked into the file header as a comment for traceability.
|
|
81
129
|
* @returns {string}
|
|
82
130
|
*/
|
|
83
|
-
function generateNeedleEmbedWebComponent(filepath, src) {
|
|
131
|
+
function generateNeedleEmbedWebComponent(filepath, src, apiFacadeFile, publicKey) {
|
|
84
132
|
|
|
85
133
|
|
|
86
134
|
// filepath is e.g. `assets/index-XXXXXXXX.js`
|
|
@@ -89,8 +137,27 @@ function generateNeedleEmbedWebComponent(filepath, src) {
|
|
|
89
137
|
|
|
90
138
|
const componentDefaultName = 'needle-app';
|
|
91
139
|
|
|
92
|
-
|
|
140
|
+
// Bake the asset path in as a safely-escaped string literal: paths can contain quotes/spaces/backticks,
|
|
141
|
+
// and JSON string literals are valid JS string literals. No asset → serializes to the literal `null`,
|
|
142
|
+
// matching the previous default. Actual origin resolution happens at runtime via `new URL(...)` below.
|
|
143
|
+
const defaultSrc = JSON.stringify(src ?? null);
|
|
144
|
+
|
|
145
|
+
// Re-export the engine API facade so users have a SINGLE import path: the same URL they load
|
|
146
|
+
// needle-app.js from also serves the engine hooks (`import { onStart } from "<deploy>/needle-app.js"`).
|
|
147
|
+
// The facade is a separate, deterministically-named chunk that shares the app's engine instance,
|
|
148
|
+
// so the re-export resolves to the live scene. Specifier is relative to this file's own URL.
|
|
149
|
+
const apiReexport = apiFacadeFile
|
|
150
|
+
? `export * from ${JSON.stringify("./" + apiFacadeFile)};\n`
|
|
151
|
+
: ``;
|
|
93
152
|
|
|
153
|
+
// File header for traceability: which build produced this file, and (if available) the public project id.
|
|
154
|
+
// Values are sanitized to a single line so they cannot break out of the `//` comment.
|
|
155
|
+
const oneLine = (/** @type {string} */ v) => String(v).replace(/[\r\n]+/g, " ").trim();
|
|
156
|
+
let header = `// needle-app.js - generated by the @needle-tools/engine needle-app plugin\n`;
|
|
157
|
+
header += `// build time: ${oneLine(new Date().toISOString())}\n`;
|
|
158
|
+
if (publicKey) header += `// public key: ${oneLine(publicKey)}\n`;
|
|
159
|
+
|
|
160
|
+
return `${header}${apiReexport}
|
|
94
161
|
// Needle Engine attributes we want to allow to be overriden
|
|
95
162
|
const knownAttributes = [
|
|
96
163
|
"src",
|
|
@@ -170,8 +237,14 @@ if (!customElements.get(componentName)) {
|
|
|
170
237
|
updateAttributes() {
|
|
171
238
|
console.debug("NeedleApp updating attributes");
|
|
172
239
|
|
|
173
|
-
|
|
174
|
-
if(src)
|
|
240
|
+
let src = this.getAttribute('src') || ${defaultSrc};
|
|
241
|
+
if(src) {
|
|
242
|
+
// Resolve the asset URL against this script's own origin (import.meta.url) so relative
|
|
243
|
+
// paths keep resolving to the deployed app even when <needle-app> is embedded on a
|
|
244
|
+
// different domain (e.g. Webflow). Absolute URLs pass through unchanged; spaces get encoded.
|
|
245
|
+
src = new URL(src, this.basePath + '/').href;
|
|
246
|
+
this.needleEngine.setAttribute("src", src);
|
|
247
|
+
}
|
|
175
248
|
else this.needleEngine.removeAttribute("src");
|
|
176
249
|
|
|
177
250
|
for(const attr of knownAttributes) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether remote debugging is enabled. Toggled by the `remote` user setting or the
|
|
3
|
+
* `NEEDLE_REMOTE` env var (the env var wins so you can flip it without editing the
|
|
4
|
+
* config: `NEEDLE_REMOTE=1 npm run dev`, or `NEEDLE_REMOTE=false` to force off).
|
|
5
|
+
* @param {import('../types/userconfig.js').userSettings} [userSettings]
|
|
6
|
+
* @returns {boolean}
|
|
7
|
+
*/
|
|
8
|
+
export function isRemoteEnabled(userSettings?: import("../types/userconfig.js").userSettings): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Remote debugging plugin.
|
|
11
|
+
*
|
|
12
|
+
* Binds the Vite dev server to the LAN (`server.host = true`) so other devices can
|
|
13
|
+
* reach it, and prints the network URL + a scannable QR code once the server is
|
|
14
|
+
* listening. HTTPS itself is provided by `@vitejs/plugin-basic-ssl`, which
|
|
15
|
+
* {@link needlePlugins} adds to the plugin array when remote is enabled (WebXR and
|
|
16
|
+
* other secure‑context features on the device require https).
|
|
17
|
+
*
|
|
18
|
+
* The page's console / errors / device info already stream back to the dev server
|
|
19
|
+
* via the Needle logger (`needleLogger` + `logger.client.js`) over Vite's
|
|
20
|
+
* websocket, landing in `node_modules/.needle/logs/latest.client.needle.log`. This
|
|
21
|
+
* plugin is read‑only: it does not (yet) inject commands back into the page.
|
|
22
|
+
*
|
|
23
|
+
* @param {"build" | "serve" | undefined} command
|
|
24
|
+
* @param {unknown} _config
|
|
25
|
+
* @param {import('../types/userconfig.js').userSettings} [userSettings]
|
|
26
|
+
* @returns {import('vite').Plugin | null}
|
|
27
|
+
*/
|
|
28
|
+
export function needleRemote(command: "build" | "serve" | undefined, _config: unknown, userSettings?: import("../types/userconfig.js").userSettings): import("vite").Plugin | null;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import os from "os";
|
|
3
|
+
import { needleLog } from "./logging.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Whether remote debugging is enabled. Toggled by the `remote` user setting or the
|
|
7
|
+
* `NEEDLE_REMOTE` env var (the env var wins so you can flip it without editing the
|
|
8
|
+
* config: `NEEDLE_REMOTE=1 npm run dev`, or `NEEDLE_REMOTE=false` to force off).
|
|
9
|
+
* @param {import('../types/userconfig.js').userSettings} [userSettings]
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export function isRemoteEnabled(userSettings) {
|
|
13
|
+
const env = process.env.NEEDLE_REMOTE;
|
|
14
|
+
if (env !== undefined && env !== "") {
|
|
15
|
+
const v = env.toLowerCase();
|
|
16
|
+
return !(v === "false" || v === "0" || v === "off" || v === "no");
|
|
17
|
+
}
|
|
18
|
+
const r = userSettings?.remote;
|
|
19
|
+
return r === true || (typeof r === "object" && r !== null);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Pick the first non-internal IPv4 LAN address — the one a phone / headset on the
|
|
24
|
+
* same Wi‑Fi can reach. Prefers the common private ranges and otherwise falls back
|
|
25
|
+
* to the first external IPv4 found.
|
|
26
|
+
* @returns {string | null}
|
|
27
|
+
*/
|
|
28
|
+
function lanAddress() {
|
|
29
|
+
const ifaces = os.networkInterfaces();
|
|
30
|
+
/** @type {string | null} */
|
|
31
|
+
let fallback = null;
|
|
32
|
+
for (const name of Object.keys(ifaces)) {
|
|
33
|
+
for (const net of ifaces[name] || []) {
|
|
34
|
+
// Node reports family as the string "IPv4" (older versions: the number 4)
|
|
35
|
+
if (net.family !== "IPv4" && /** @type {unknown} */ (net.family) !== 4) continue;
|
|
36
|
+
if (net.internal) continue;
|
|
37
|
+
if (/^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(net.address)) return net.address;
|
|
38
|
+
if (!fallback) fallback = net.address;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Remote debugging plugin.
|
|
46
|
+
*
|
|
47
|
+
* Binds the Vite dev server to the LAN (`server.host = true`) so other devices can
|
|
48
|
+
* reach it, and prints the network URL + a scannable QR code once the server is
|
|
49
|
+
* listening. HTTPS itself is provided by `@vitejs/plugin-basic-ssl`, which
|
|
50
|
+
* {@link needlePlugins} adds to the plugin array when remote is enabled (WebXR and
|
|
51
|
+
* other secure‑context features on the device require https).
|
|
52
|
+
*
|
|
53
|
+
* The page's console / errors / device info already stream back to the dev server
|
|
54
|
+
* via the Needle logger (`needleLogger` + `logger.client.js`) over Vite's
|
|
55
|
+
* websocket, landing in `node_modules/.needle/logs/latest.client.needle.log`. This
|
|
56
|
+
* plugin is read‑only: it does not (yet) inject commands back into the page.
|
|
57
|
+
*
|
|
58
|
+
* @param {"build" | "serve" | undefined} command
|
|
59
|
+
* @param {unknown} _config
|
|
60
|
+
* @param {import('../types/userconfig.js').userSettings} [userSettings]
|
|
61
|
+
* @returns {import('vite').Plugin | null}
|
|
62
|
+
*/
|
|
63
|
+
export function needleRemote(command, _config, userSettings) {
|
|
64
|
+
if (command === "build") return null;
|
|
65
|
+
if (!isRemoteEnabled(userSettings)) return null;
|
|
66
|
+
|
|
67
|
+
const r = userSettings?.remote;
|
|
68
|
+
const showQr = !(typeof r === "object" && r !== null && r.qr === false);
|
|
69
|
+
let announced = false;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
name: "needle:remote",
|
|
73
|
+
enforce: "pre",
|
|
74
|
+
config(config) {
|
|
75
|
+
if (!config.server) config.server = {};
|
|
76
|
+
// Bind 0.0.0.0 so phones / headsets on the same network can connect.
|
|
77
|
+
// Respect an explicit host the user already set.
|
|
78
|
+
if (config.server.host === undefined) config.server.host = true;
|
|
79
|
+
},
|
|
80
|
+
configureServer(server) {
|
|
81
|
+
const announce = async () => {
|
|
82
|
+
if (announced) return;
|
|
83
|
+
const addr = server.httpServer?.address?.();
|
|
84
|
+
const port = (addr && typeof addr === "object" && addr.port) || server.config.server.port;
|
|
85
|
+
const ip = lanAddress();
|
|
86
|
+
if (!port || !ip) {
|
|
87
|
+
needleLog("needle-remote", "Remote debugging is ON but no LAN address was found — is this machine on a network?", "warn");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
announced = true;
|
|
91
|
+
const protocol = server.config.server.https ? "https" : "http";
|
|
92
|
+
const url = `${protocol}://${ip}:${port}/`;
|
|
93
|
+
const note = protocol === "https"
|
|
94
|
+
? "\n (accept the one‑time self‑signed certificate warning on the device)"
|
|
95
|
+
: "\n (no HTTPS — install @vitejs/plugin-basic-ssl for WebXR / secure‑context features)";
|
|
96
|
+
needleLog("needle-remote", `Remote debugging is ON — open on a device on the same Wi‑Fi:\n ${url}${note}`);
|
|
97
|
+
// The device's console / errors stream back over Vite's HMR websocket
|
|
98
|
+
// (via the Needle logger). With HMR disabled there's no socket, so the
|
|
99
|
+
// logs would silently never arrive — warn rather than leave you guessing.
|
|
100
|
+
if (server.config.server.hmr === false) {
|
|
101
|
+
needleLog("needle-remote", "HMR is disabled, so device console logs won't stream back (they ride the HMR websocket). Enable HMR to capture remote logs.", "warn");
|
|
102
|
+
}
|
|
103
|
+
if (showQr) await printQr(url);
|
|
104
|
+
};
|
|
105
|
+
// `listening` fires once the port is bound; resolve the real port from it.
|
|
106
|
+
server.httpServer?.once("listening", announce);
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Print a scannable QR code of the URL to the terminal (best effort — needs the
|
|
113
|
+
* optional `qrcode-terminal` package; if it's missing we just say how to get it).
|
|
114
|
+
* @param {string} url
|
|
115
|
+
*/
|
|
116
|
+
async function printQr(url) {
|
|
117
|
+
try {
|
|
118
|
+
const mod = await import("qrcode-terminal");
|
|
119
|
+
const qrcode = /** @type {{ generate: (text: string, opts?: object) => void }} */ (mod.default || mod);
|
|
120
|
+
qrcode.generate(url, { small: true });
|
|
121
|
+
} catch {
|
|
122
|
+
needleLog("needle-remote", "Install `qrcode-terminal` to show a scannable QR code here: npm i -D qrcode-terminal", "log");
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/engine/api.ts
CHANGED
|
@@ -230,7 +230,7 @@ export * from "./engine_input.js";
|
|
|
230
230
|
export { InstancingUtil } from "./engine_instancing.js";
|
|
231
231
|
|
|
232
232
|
/** License checking utilities */
|
|
233
|
-
export {
|
|
233
|
+
export { _imGyfK, _QchMjAD, rkHTARX } from "./engine_license.js";
|
|
234
234
|
|
|
235
235
|
|
|
236
236
|
// ============================================================================
|
|
@@ -1,25 +1,78 @@
|
|
|
1
1
|
import { AudioContext } from "three";
|
|
2
2
|
|
|
3
3
|
import { Application } from "./engine_application.js";
|
|
4
|
+
import { getParam } from "./engine_utils.js";
|
|
4
5
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
/** Verbose audio logging, opt-in via `?debugaudio` (same flag AudioSource uses). */
|
|
7
|
+
const debug = getParam("debugaudio");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* iOS Safari exposes a non-standard `"interrupted"` state in addition to the
|
|
11
|
+
* spec'd `AudioContextState`s when the audio session is taken away (e.g. the
|
|
12
|
+
* device is locked or another app starts playing audio).
|
|
13
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/state#resuming_interrupted_play_states_in_ios_safari
|
|
14
|
+
*/
|
|
15
|
+
export type InterruptibleAudioContextState = AudioContextState | "interrupted";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @internal
|
|
19
|
+
* Pure policy deciding whether `AudioContext.resume()` should be attempted right now.
|
|
20
|
+
*
|
|
21
|
+
* The context needs resuming when it is `"suspended"` or (on iOS) `"interrupted"`.
|
|
22
|
+
* But resuming is only allowed while the document is **visible**: on iOS Safari,
|
|
23
|
+
* calling `resume()` while the page is backgrounded or the device is locked fails
|
|
24
|
+
* with `InvalidStateError: Failed to start the audio device`, because the audio
|
|
25
|
+
* hardware cannot be started in the background. In that case the resume must be
|
|
26
|
+
* deferred until the page becomes visible again.
|
|
27
|
+
*
|
|
28
|
+
* @param state The current (possibly iOS-interrupted) audio context state.
|
|
29
|
+
* @param visibility The current `document.visibilityState`.
|
|
30
|
+
* @returns `true` if `resume()` should be called now, `false` if it should be deferred.
|
|
31
|
+
*/
|
|
32
|
+
export function shouldResumeAudioContext(
|
|
33
|
+
state: InterruptibleAudioContextState,
|
|
34
|
+
visibility: DocumentVisibilityState,
|
|
35
|
+
): boolean {
|
|
36
|
+
// Resuming while hidden throws InvalidStateError on iOS — defer until visible.
|
|
37
|
+
if (visibility !== "visible") return false;
|
|
38
|
+
return state === "suspended" || state === "interrupted";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Resume the shared AudioContext if it is suspended/interrupted and the page is visible. */
|
|
42
|
+
function recoverAudioContext() {
|
|
43
|
+
const ctx = AudioContext.getContext();
|
|
44
|
+
const state = ctx.state as InterruptibleAudioContextState;
|
|
45
|
+
const visibility = typeof document !== "undefined" ? document.visibilityState : "visible";
|
|
46
|
+
if (!shouldResumeAudioContext(state, visibility)) return;
|
|
47
|
+
ctx.resume()
|
|
48
|
+
.then(() => { if (debug) console.log("AudioContext resumed successfully"); })
|
|
49
|
+
.catch(e => { if (debug) console.warn("Failed to resume AudioContext: " + e); });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @internal
|
|
54
|
+
* Keep the shared AudioContext alive across suspends/interruptions (tab switch, device lock, audio
|
|
55
|
+
* session interruption). On returning to the foreground we simply resume() the EXISTING context —
|
|
56
|
+
* the same approach three.js uses, which recovers cleanly because AudioSource plays clips through a
|
|
57
|
+
* MediaElementAudioSourceNode (an <audio> element), whose media session iOS keeps alive across a
|
|
58
|
+
* lock. We deliberately do NOT close/recreate the context: on-device testing showed that makes iOS
|
|
59
|
+
* worse (a recreated context's output stays dead) and it is unnecessary.
|
|
60
|
+
*/
|
|
8
61
|
export function ensureAudioContextIsResumed() {
|
|
9
62
|
Application.registerWaitForInteraction(() => {
|
|
10
|
-
//
|
|
63
|
+
// https://github.com/mrdoob/three.js/issues/27779 & https://linear.app/needle/issue/NE-4257
|
|
11
64
|
const ctx = AudioContext.getContext();
|
|
12
|
-
ctx.addEventListener("statechange", () =>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
}
|
|
65
|
+
ctx.addEventListener("statechange", () => setTimeout(recoverAudioContext, 300));
|
|
66
|
+
|
|
67
|
+
// iOS frequently rejects the first resume() right after foregrounding with InvalidStateError,
|
|
68
|
+
// then accepts one a few hundred ms later with no follow-up statechange — so retry with a
|
|
69
|
+
// short backoff. Each attempt is a no-op once running.
|
|
70
|
+
if (typeof document !== "undefined") {
|
|
71
|
+
document.addEventListener("visibilitychange", () => {
|
|
72
|
+
if (document.visibilityState !== "visible") return;
|
|
73
|
+
for (const delay of [0, 250, 600, 1200, 2000]) setTimeout(recoverAudioContext, delay);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
23
76
|
});
|
|
24
77
|
}
|
|
25
78
|
|
|
@@ -8,7 +8,7 @@ import { initBuiltinTypes } from "./codegen/register_types.js";
|
|
|
8
8
|
import { initSpatialConsole } from "./debug/debug_spatial_console.js";
|
|
9
9
|
import { initAddressableSerializers } from "./engine_addressables.js";
|
|
10
10
|
import { ensureAudioContextIsResumed } from "./engine_audio.js";
|
|
11
|
-
import {
|
|
11
|
+
import { _NHJZozV } from "./engine_license.js";
|
|
12
12
|
import { initNeedleLoader } from "./engine_loaders.js";
|
|
13
13
|
import { initPhysics } from "./engine_physics_rapier.js";
|
|
14
14
|
import { initBuiltinSerializers } from "./engine_serialization_builtin_serializer.js";
|
|
@@ -59,7 +59,7 @@ export function initEngine() {
|
|
|
59
59
|
initPhysics();
|
|
60
60
|
initXR();
|
|
61
61
|
initSpatialConsole();
|
|
62
|
-
|
|
62
|
+
_NHJZozV();
|
|
63
63
|
|
|
64
64
|
// Must be last. customElements.define() upgrades existing <needle-engine>
|
|
65
65
|
// elements synchronously, which can trigger context.create() and dispatch
|
|
@@ -137,19 +137,39 @@ export function resolveInstanceReferences(objectMap: InstantiateReferenceMap): v
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Per-call traversal counters. Only populated when `?debuginstantiate` is set; null otherwise so
|
|
142
|
+
* the hot path stays allocation-free. Safe as module state because resolution runs synchronously
|
|
143
|
+
* and these calls never nest.
|
|
144
|
+
*/
|
|
145
|
+
type ResolveStats = { objects: number; components: number; props: number; remapped: number };
|
|
146
|
+
let _resolveStats: ResolveStats | null = null;
|
|
147
|
+
|
|
140
148
|
/**
|
|
141
149
|
* Resolves string-based guid references in all components of a hierarchy using a GuidsMap.
|
|
142
150
|
* Used by the glTF loading path where objects get new guids assigned and string references
|
|
143
151
|
* (e.g. PlayableDirector track.outputs) need to be updated.
|
|
144
152
|
*/
|
|
145
153
|
export function resolveStringGuidsInHierarchy(root: Object3D, guidsMap: GuidsMap): void {
|
|
154
|
+
if (!debug) {
|
|
155
|
+
resolveStringGuidsRecursive(root, guidsMap);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
_resolveStats = { objects: 0, components: 0, props: 0, remapped: 0 };
|
|
159
|
+
const start = (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
146
160
|
resolveStringGuidsRecursive(root, guidsMap);
|
|
161
|
+
const ms = (typeof performance !== "undefined" ? performance.now() : Date.now()) - start;
|
|
162
|
+
const s = _resolveStats;
|
|
163
|
+
_resolveStats = null;
|
|
164
|
+
console.log(`[resolveStringGuids] root="${root.name}" objects=${s.objects} components=${s.components} propsVisited=${s.props} remapped=${s.remapped} mapSize=${Object.keys(guidsMap).length} in ${ms.toFixed(2)}ms`);
|
|
147
165
|
}
|
|
148
166
|
|
|
149
167
|
function resolveStringGuidsRecursive(obj: Object3D, guidsMap: GuidsMap): void {
|
|
168
|
+
if (_resolveStats) _resolveStats.objects++;
|
|
150
169
|
if (obj.userData?.components) {
|
|
151
170
|
for (const component of obj.userData.components) {
|
|
152
171
|
if (component === null) continue;
|
|
172
|
+
if (_resolveStats) _resolveStats.components++;
|
|
153
173
|
resolveStringGuidsInObject(component, guidsMap);
|
|
154
174
|
}
|
|
155
175
|
}
|
|
@@ -166,26 +186,33 @@ function resolveStringGuidsInObject(obj: any, guidsMap: GuidsMap, visited?: Weak
|
|
|
166
186
|
visited.add(obj);
|
|
167
187
|
|
|
168
188
|
for (const key of Object.keys(obj)) {
|
|
189
|
+
if (_resolveStats) _resolveStats.props++;
|
|
169
190
|
const value = obj[key];
|
|
170
191
|
if (value === null || value === undefined) continue;
|
|
171
192
|
if (typeof value === "string") {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
193
|
+
const mapped = guidsMap[value];
|
|
194
|
+
if (mapped) { obj[key] = mapped; if (_resolveStats) _resolveStats.remapped++; }
|
|
175
195
|
}
|
|
176
196
|
else if (Array.isArray(value)) {
|
|
177
197
|
for (let i = 0; i < value.length; i++) {
|
|
178
|
-
|
|
179
|
-
|
|
198
|
+
const entry = value[i];
|
|
199
|
+
if (typeof entry === "string") {
|
|
200
|
+
const mapped = guidsMap[entry];
|
|
201
|
+
if (mapped) { value[i] = mapped; if (_resolveStats) _resolveStats.remapped++; }
|
|
180
202
|
}
|
|
181
|
-
|
|
182
|
-
|
|
203
|
+
// Only descend into plain data (arrays and plain objects). See isPlainObject note below.
|
|
204
|
+
else if (Array.isArray(entry) || isPlainObject(entry)) {
|
|
205
|
+
resolveStringGuidsInObject(entry, guidsMap, visited);
|
|
183
206
|
}
|
|
184
207
|
}
|
|
185
208
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
209
|
+
// Only descend into plain objects. String guids only ever live in serialized plain-object
|
|
210
|
+
// data (e.g. PlayableDirector track.outputs). We must NOT traverse class instances —
|
|
211
|
+
// Object3D, Component, three.js value/render types (Material, Texture, BufferGeometry) or
|
|
212
|
+
// TypedArrays. Descending into the scene graph or geometry buffers (e.g. Object.keys on a
|
|
213
|
+
// multi-million-element Float32Array) makes hierarchy guid resolution scale with mesh data
|
|
214
|
+
// and dominates load time.
|
|
215
|
+
else if (isPlainObject(value)) {
|
|
189
216
|
resolveStringGuidsInObject(value, guidsMap, visited);
|
|
190
217
|
}
|
|
191
218
|
}
|