@needle-tools/engine 4.14.0 → 4.15.0-next.f391a30

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 (198) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/components.needle.json +1 -1
  3. package/dist/{gltf-progressive-BttGBXw6.umd.cjs → gltf-progressive-CMwJPwEt.umd.cjs} +1 -1
  4. package/dist/{gltf-progressive-Bm_6aEi4.js → gltf-progressive-CTlvpS3A.js} +1 -1
  5. package/dist/{gltf-progressive-T5WKTux5.min.js → gltf-progressive-DYL3SLVb.min.js} +1 -1
  6. package/dist/materialx-4jJLLe9Q.js +4174 -0
  7. package/dist/materialx-Bt9FHwco.min.js +158 -0
  8. package/dist/materialx-NDD0y4JY.umd.cjs +158 -0
  9. package/dist/{needle-engine.bundle-COL2Bar3.umd.cjs → needle-engine.bundle-C1BFRZDF.umd.cjs} +150 -140
  10. package/dist/{needle-engine.bundle-Z_gAD7Kg.js → needle-engine.bundle-DB4kLWO_.js} +6651 -6400
  11. package/dist/{needle-engine.bundle-NolzHLqO.min.js → needle-engine.bundle-DsTdfmeb.min.js} +151 -141
  12. package/dist/needle-engine.d.ts +345 -88
  13. package/dist/needle-engine.js +322 -322
  14. package/dist/needle-engine.min.js +1 -1
  15. package/dist/needle-engine.umd.cjs +1 -1
  16. package/dist/{postprocessing-06AXuvdv.min.js → postprocessing-BN-f4viE.min.js} +1 -1
  17. package/dist/{postprocessing-CPDcA21P.umd.cjs → postprocessing-DYmYOVm4.umd.cjs} +1 -1
  18. package/dist/{postprocessing-CI2x8Cln.js → postprocessing-De9ZpJrk.js} +1 -1
  19. package/dist/{three-examples-BMmNgNCN.umd.cjs → three-examples-BHqRVpO_.umd.cjs} +12 -12
  20. package/dist/{three-examples-CMYCd5nH.js → three-examples-C0ZCCA_K.js} +182 -192
  21. package/dist/{three-examples-CQl1fFZp.min.js → three-examples-DmTY8tGr.min.js} +14 -14
  22. package/lib/engine/api.d.ts +0 -2
  23. package/lib/engine/api.js +0 -2
  24. package/lib/engine/api.js.map +1 -1
  25. package/lib/engine/debug/debug.js +1 -1
  26. package/lib/engine/debug/debug.js.map +1 -1
  27. package/lib/engine/debug/debug_spatial_console.js +1 -1
  28. package/lib/engine/debug/debug_spatial_console.js.map +1 -1
  29. package/lib/engine/engine_accessibility.d.ts +77 -0
  30. package/lib/engine/engine_accessibility.js +162 -0
  31. package/lib/engine/engine_accessibility.js.map +1 -0
  32. package/lib/engine/engine_context.d.ts +2 -0
  33. package/lib/engine/engine_context.js +8 -1
  34. package/lib/engine/engine_context.js.map +1 -1
  35. package/lib/engine/engine_create_objects.js +1 -1
  36. package/lib/engine/engine_create_objects.js.map +1 -1
  37. package/lib/engine/engine_gizmos.js +1 -1
  38. package/lib/engine/engine_gizmos.js.map +1 -1
  39. package/lib/engine/engine_license.js +7 -2
  40. package/lib/engine/engine_license.js.map +1 -1
  41. package/lib/engine/engine_materialpropertyblock.d.ts +90 -4
  42. package/lib/engine/engine_materialpropertyblock.js +97 -7
  43. package/lib/engine/engine_materialpropertyblock.js.map +1 -1
  44. package/lib/engine/engine_math.d.ts +34 -1
  45. package/lib/engine/engine_math.js +34 -1
  46. package/lib/engine/engine_math.js.map +1 -1
  47. package/lib/engine/engine_networking.js +1 -1
  48. package/lib/engine/engine_networking.js.map +1 -1
  49. package/lib/engine/engine_types.d.ts +2 -0
  50. package/lib/engine/engine_types.js +2 -0
  51. package/lib/engine/engine_types.js.map +1 -1
  52. package/lib/engine/engine_utils.js +2 -2
  53. package/lib/engine/engine_utils.js.map +1 -1
  54. package/lib/engine/export/gltf/EXT_mesh_gpu_instancing_exporter.js.map +1 -0
  55. package/lib/engine/export/gltf/index.js +1 -1
  56. package/lib/engine/export/gltf/index.js.map +1 -1
  57. package/lib/engine/webcomponents/icons.js +3 -0
  58. package/lib/engine/webcomponents/icons.js.map +1 -1
  59. package/lib/engine/webcomponents/logo-element.d.ts +7 -3
  60. package/lib/engine/webcomponents/logo-element.js +21 -1
  61. package/lib/engine/webcomponents/logo-element.js.map +1 -1
  62. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
  63. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
  64. package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +10 -7
  65. package/lib/engine/webcomponents/needle menu/needle-menu.js +14 -4
  66. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  67. package/lib/engine/webcomponents/needle-button.d.ts +37 -11
  68. package/lib/engine/webcomponents/needle-button.js +42 -11
  69. package/lib/engine/webcomponents/needle-button.js.map +1 -1
  70. package/lib/engine/webcomponents/needle-engine.ar-overlay.js +10 -1
  71. package/lib/engine/webcomponents/needle-engine.ar-overlay.js.map +1 -1
  72. package/lib/engine/webcomponents/needle-engine.d.ts +13 -2
  73. package/lib/engine/webcomponents/needle-engine.js +23 -3
  74. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  75. package/lib/engine-components/Component.d.ts +1 -2
  76. package/lib/engine-components/Component.js +1 -3
  77. package/lib/engine-components/Component.js.map +1 -1
  78. package/lib/engine-components/DragControls.d.ts +1 -0
  79. package/lib/engine-components/DragControls.js +21 -0
  80. package/lib/engine-components/DragControls.js.map +1 -1
  81. package/lib/engine-components/NeedleMenu.d.ts +2 -0
  82. package/lib/engine-components/NeedleMenu.js +2 -0
  83. package/lib/engine-components/NeedleMenu.js.map +1 -1
  84. package/lib/engine-components/Networking.d.ts +28 -3
  85. package/lib/engine-components/Networking.js +28 -3
  86. package/lib/engine-components/Networking.js.map +1 -1
  87. package/lib/engine-components/ReflectionProbe.d.ts +25 -2
  88. package/lib/engine-components/ReflectionProbe.js +46 -2
  89. package/lib/engine-components/ReflectionProbe.js.map +1 -1
  90. package/lib/engine-components/Skybox.js +4 -2
  91. package/lib/engine-components/Skybox.js.map +1 -1
  92. package/lib/engine-components/export/gltf/GltfExport.js +1 -1
  93. package/lib/engine-components/export/gltf/GltfExport.js.map +1 -1
  94. package/lib/engine-components/export/usdz/ThreeUSDZExporter.js +2 -2
  95. package/lib/engine-components/export/usdz/USDZExporter.js +1 -1
  96. package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
  97. package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.d.ts +15 -0
  98. package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.js +77 -0
  99. package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.js.map +1 -1
  100. package/lib/engine-components/export/usdz/extensions/behavior/PhysicsExtension.js +2 -2
  101. package/lib/engine-components/export/usdz/extensions/behavior/PhysicsExtension.js.map +1 -1
  102. package/lib/engine-components/postprocessing/Effects/Tonemapping.utils.d.ts +1 -1
  103. package/lib/engine-components/ui/Button.d.ts +1 -0
  104. package/lib/engine-components/ui/Button.js +11 -0
  105. package/lib/engine-components/ui/Button.js.map +1 -1
  106. package/lib/engine-components/ui/Text.d.ts +1 -0
  107. package/lib/engine-components/ui/Text.js +11 -0
  108. package/lib/engine-components/ui/Text.js.map +1 -1
  109. package/package.json +18 -14
  110. package/plugins/common/buildinfo.js +46 -10
  111. package/plugins/common/files.js +2 -1
  112. package/plugins/common/license.js +144 -69
  113. package/plugins/common/logger.js +172 -11
  114. package/plugins/common/needle-engine-skill.md +175 -0
  115. package/plugins/common/worker.js +5 -4
  116. package/plugins/types/userconfig.d.ts +40 -2
  117. package/plugins/vite/ai.js +71 -0
  118. package/plugins/vite/alias.js +6 -5
  119. package/plugins/vite/asap.js +6 -5
  120. package/plugins/vite/build-pipeline.js +224 -41
  121. package/plugins/vite/buildinfo.js +66 -6
  122. package/plugins/vite/copyfiles.js +41 -12
  123. package/plugins/vite/custom-element-data.js +26 -16
  124. package/plugins/vite/defines.js +8 -5
  125. package/plugins/vite/dependencies.js +16 -10
  126. package/plugins/vite/dependency-watcher.js +35 -7
  127. package/plugins/vite/drop-client.js +7 -5
  128. package/plugins/vite/drop.js +16 -14
  129. package/plugins/vite/editor-connection.js +18 -16
  130. package/plugins/vite/imports-logger.js +12 -2
  131. package/plugins/vite/index.js +8 -3
  132. package/plugins/vite/local-files-analysis.js +789 -0
  133. package/plugins/vite/local-files-core.js +992 -0
  134. package/plugins/vite/local-files-internals.js +28 -0
  135. package/plugins/vite/local-files-types.d.ts +111 -0
  136. package/plugins/vite/local-files-utils.js +359 -0
  137. package/plugins/vite/local-files.js +2 -441
  138. package/plugins/vite/logger.client.js +45 -35
  139. package/plugins/vite/logger.js +6 -3
  140. package/plugins/vite/logging.js +129 -0
  141. package/plugins/vite/meta.js +18 -4
  142. package/plugins/vite/needle-app.js +4 -3
  143. package/plugins/vite/peer.js +2 -1
  144. package/plugins/vite/pwa.js +33 -17
  145. package/plugins/vite/reload.js +24 -2
  146. package/src/engine/api.ts +0 -3
  147. package/src/engine/debug/debug.ts +1 -1
  148. package/src/engine/debug/debug_spatial_console.ts +5 -1
  149. package/src/engine/engine_accessibility.ts +198 -0
  150. package/src/engine/engine_context.ts +10 -1
  151. package/src/engine/engine_create_objects.ts +1 -1
  152. package/src/engine/engine_gizmos.ts +9 -5
  153. package/src/engine/engine_license.ts +7 -2
  154. package/src/engine/engine_materialpropertyblock.ts +102 -11
  155. package/src/engine/engine_math.ts +34 -1
  156. package/src/engine/engine_networking.ts +1 -1
  157. package/src/engine/engine_types.ts +5 -0
  158. package/src/engine/engine_utils.ts +2 -2
  159. package/src/engine/export/gltf/index.ts +1 -1
  160. package/src/engine/webcomponents/icons.ts +3 -0
  161. package/src/engine/webcomponents/logo-element.ts +24 -4
  162. package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +6 -2
  163. package/src/engine/webcomponents/needle menu/needle-menu.ts +23 -11
  164. package/src/engine/webcomponents/needle-button.ts +44 -13
  165. package/src/engine/webcomponents/needle-engine.ar-overlay.ts +13 -2
  166. package/src/engine/webcomponents/needle-engine.ts +31 -8
  167. package/src/engine-components/Component.ts +2 -5
  168. package/src/engine-components/DragControls.ts +29 -4
  169. package/src/engine-components/NeedleMenu.ts +5 -3
  170. package/src/engine-components/Networking.ts +29 -4
  171. package/src/engine-components/ReflectionProbe.ts +52 -9
  172. package/src/engine-components/Skybox.ts +4 -2
  173. package/src/engine-components/export/gltf/GltfExport.ts +1 -1
  174. package/src/engine-components/export/usdz/ThreeUSDZExporter.ts +2 -2
  175. package/src/engine-components/export/usdz/USDZExporter.ts +1 -1
  176. package/src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +108 -32
  177. package/src/engine-components/export/usdz/extensions/behavior/PhysicsExtension.ts +2 -2
  178. package/src/engine-components/ui/Button.ts +12 -0
  179. package/src/engine-components/ui/Text.ts +13 -0
  180. package/dist/materialx-CJyQZtjt.min.js +0 -90
  181. package/dist/materialx-DMs1E08Z.js +0 -4636
  182. package/dist/materialx-DaKKOoVk.umd.cjs +0 -90
  183. package/lib/engine/engine_test_utils.d.ts +0 -39
  184. package/lib/engine/engine_test_utils.js +0 -84
  185. package/lib/engine/engine_test_utils.js.map +0 -1
  186. package/lib/include/three/EXT_mesh_gpu_instancing_exporter.js.map +0 -1
  187. package/src/engine/engine_test_utils.ts +0 -109
  188. package/src/include/draco/draco_decoder.js +0 -34
  189. package/src/include/draco/draco_decoder.wasm +0 -0
  190. package/src/include/draco/draco_wasm_wrapper.js +0 -117
  191. package/src/include/ktx2/basis_transcoder.js +0 -19
  192. package/src/include/ktx2/basis_transcoder.wasm +0 -0
  193. package/src/include/needle/arial-msdf.json +0 -1472
  194. package/src/include/needle/arial.png +0 -0
  195. package/src/include/needle/poweredbyneedle.webp +0 -0
  196. /package/lib/{include/three → engine/export/gltf}/EXT_mesh_gpu_instancing_exporter.d.ts +0 -0
  197. /package/lib/{include/three → engine/export/gltf}/EXT_mesh_gpu_instancing_exporter.js +0 -0
  198. /package/src/{include/three → engine/export/gltf}/EXT_mesh_gpu_instancing_exporter.js +0 -0
@@ -0,0 +1,789 @@
1
+ // @ts-check
2
+ import { existsSync, readFileSync, readdirSync, openSync, readSync, closeSync } from 'fs';
3
+ import { join, relative } from 'path';
4
+ import { needleBlue, needleDim, needleLog, needleSupportsColor } from './logging.js';
5
+
6
+ /** @typedef {import('./local-files-types.js').LocalizationOptions} LocalizationOptions */
7
+ /** @typedef {import('./local-files-types.js').AutoPolicy} AutoPolicy */
8
+ /** @typedef {import('./local-files-types.js').SceneAnalysisReport} SceneAnalysisReport */
9
+ /** @typedef {import('./local-files-types.js').SceneFile} SceneFile */
10
+ /** @typedef {import('./local-files-types.js').UrlHandler} UrlHandler */
11
+ /** @typedef {import('./local-files-types.js').LocalizationContext} LocalizationContext */
12
+
13
+ const NEEDLE_COMPONENTS_EXTENSION = "NEEDLE_components";
14
+ export const SKYBOX_MAGIC_KEYWORDS = ["studio", "blurred-skybox", "quicklook", "quicklook-ar"];
15
+ const SKYBOX_BASE_URL = "https://cdn.needle.tools/static/skybox/";
16
+ const HLS_CDN_SEGMENT = "/npm/hls.js@";
17
+
18
+ /** @type {Map<string, {autoPolicy: AutoPolicy|null, report: SceneAnalysisReport, hasLogged: boolean}>} */
19
+ const analysisByProject = new Map();
20
+
21
+ /**
22
+ * @param {string} command
23
+ * @param {unknown} _config
24
+ * @param {import('../types').userSettings} userSettings
25
+ */
26
+ export function needleLocalFilesSceneAnalysis(command, _config, userSettings) {
27
+ if (!makeFilesLocalIsEnabled(userSettings)) return null;
28
+
29
+ const options = resolveOptions(userSettings);
30
+ let pluginRoot = process.cwd();
31
+ let pluginCommand = command;
32
+
33
+ return {
34
+ name: "needle:local-files-scene-analysis",
35
+ enforce: "pre",
36
+ /** @param {import('vite').ResolvedConfig} config */
37
+ configResolved(config) {
38
+ pluginRoot = config?.root || process.cwd();
39
+ pluginCommand = config?.command || command;
40
+ },
41
+ /** @param {import('vite').ViteDevServer} server */
42
+ configureServer(server) {
43
+ const projectDir = server?.config?.root || pluginRoot || process.cwd();
44
+ ensureProjectAnalysis(projectDir, options);
45
+ logSceneAnalysisReport(projectDir, "serve");
46
+ server?.httpServer?.once("close", () => {
47
+ logSceneAnalysisReport(projectDir, "serve");
48
+ analysisByProject.delete(projectDir);
49
+ });
50
+ },
51
+ buildStart() {
52
+ const projectDir = pluginRoot || process.cwd();
53
+ ensureProjectAnalysis(projectDir, options);
54
+ },
55
+ buildEnd() {
56
+ const projectDir = pluginRoot || process.cwd();
57
+ logSceneAnalysisReport(projectDir, "build");
58
+ analysisByProject.delete(projectDir);
59
+ },
60
+ closeBundle() {
61
+ const activeCommand = pluginCommand || command;
62
+ if (activeCommand !== "serve") return;
63
+ const projectDir = pluginRoot || process.cwd();
64
+ logSceneAnalysisReport(projectDir, "serve");
65
+ analysisByProject.delete(projectDir);
66
+ }
67
+ };
68
+ }
69
+
70
+ /**
71
+ * @param {string} projectDir
72
+ * @returns {AutoPolicy | null}
73
+ */
74
+ export function getLocalFilesAutoPolicy(projectDir) {
75
+ const entry = analysisByProject.get(projectDir);
76
+ return entry?.autoPolicy ?? null;
77
+ }
78
+
79
+ /**
80
+ * @param {import('../types').userSettings | undefined | null} userSettings
81
+ * @returns {boolean}
82
+ */
83
+ export function makeFilesLocalIsEnabled(userSettings) {
84
+ if (typeof userSettings?.makeFilesLocal === "object") return userSettings?.makeFilesLocal?.enabled !== false;
85
+ if (userSettings?.makeFilesLocal === "auto") return true;
86
+ return userSettings?.makeFilesLocal === true;
87
+ }
88
+
89
+ /**
90
+ * @param {import('../types').userSettings | undefined | null} userSettings
91
+ * @returns {LocalizationOptions}
92
+ */
93
+ export function resolveOptions(userSettings) {
94
+ if (typeof userSettings?.makeFilesLocal === "object") {
95
+ const raw = userSettings.makeFilesLocal;
96
+ const opts = { ...raw };
97
+ if (!opts.exclude?.length && opts.excludeUrls?.length) {
98
+ opts.exclude = opts.excludeUrls;
99
+ }
100
+ return opts;
101
+ }
102
+ if (userSettings?.makeFilesLocal === "auto") {
103
+ return { features: "auto" };
104
+ }
105
+ return {};
106
+ }
107
+
108
+ /**
109
+ * @param {string | string[] | undefined} features
110
+ * @returns {boolean}
111
+ */
112
+ export function hasAutoFeatureSelection(features) {
113
+ if (features === "auto") return true;
114
+ if (Array.isArray(features)) return features.includes("auto");
115
+ return false;
116
+ }
117
+
118
+ /**
119
+ * @param {LocalizationOptions} options
120
+ * @param {string} projectDir
121
+ * @returns {AutoPolicy | null}
122
+ */
123
+ export function buildAutoPolicy(options, projectDir) {
124
+ if (!hasAutoFeatureSelection(options.features)) return null;
125
+ return detectAutoPolicy(projectDir, options);
126
+ }
127
+
128
+ /**
129
+ * @param {string} projectDir
130
+ * @param {LocalizationOptions} options
131
+ * @returns {{ autoPolicy: AutoPolicy|null, report: SceneAnalysisReport, hasLogged: boolean }}
132
+ */
133
+ function ensureProjectAnalysis(projectDir, options) {
134
+ const existing = analysisByProject.get(projectDir);
135
+ if (existing) return existing;
136
+
137
+ const report = analyzeProjectGlbs(projectDir);
138
+ const autoPolicy = hasAutoFeatureSelection(options.features)
139
+ ? detectAutoPolicy(projectDir, options, report)
140
+ : null;
141
+
142
+ const entry = {
143
+ autoPolicy,
144
+ report,
145
+ hasLogged: false,
146
+ };
147
+ analysisByProject.set(projectDir, entry);
148
+ return entry;
149
+ }
150
+
151
+ /**
152
+ * @param {string} projectDir
153
+ * @param {string} mode
154
+ */
155
+ function logSceneAnalysisReport(projectDir, mode) {
156
+ const entry = analysisByProject.get(projectDir);
157
+ if (!entry || entry.hasLogged) return;
158
+
159
+ const message = formatSceneAnalysisReport(entry.report, entry.autoPolicy, projectDir, mode);
160
+ needleLog("needle:local-files", message, "log", { dimBody: false });
161
+ entry.hasLogged = true;
162
+ }
163
+
164
+ /**
165
+ * @param {string} projectDir
166
+ * @param {LocalizationOptions} options
167
+ * @param {SceneAnalysisReport | null} [cachedAnalysis]
168
+ * @returns {AutoPolicy}
169
+ */
170
+ export function detectAutoPolicy(projectDir, options, cachedAnalysis = null) {
171
+ const features = /** @type {Set<string>} */ (new Set());
172
+
173
+ features.add("draco");
174
+ features.add("ktx2");
175
+ features.add("fonts");
176
+ features.add("cdn-scripts");
177
+ features.add("needle-uploads");
178
+ features.add("needle-fonts");
179
+ features.add("needle-branding");
180
+
181
+ const pkgJsonPath = join(projectDir, "package.json");
182
+ /** @type {{dependencies?: Record<string,string>, devDependencies?: Record<string,string>} | null} */
183
+ let pkgJson = null;
184
+ try {
185
+ if (existsSync(pkgJsonPath)) {
186
+ pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
187
+ }
188
+ }
189
+ catch (_e) { }
190
+
191
+ if (pkgJson) {
192
+ const allDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
193
+ if (allDeps["@needle-tools/materialx"]) {
194
+ features.add("materialx");
195
+ }
196
+ }
197
+
198
+ const indexHtmlPath = join(projectDir, "index.html");
199
+ let indexHtml = "";
200
+ try {
201
+ if (existsSync(indexHtmlPath)) {
202
+ indexHtml = readFileSync(indexHtmlPath, "utf-8");
203
+ }
204
+ }
205
+ catch (_e) { }
206
+
207
+ const indexSkyboxUrls = collectIndexHtmlSkyboxUrls(indexHtml);
208
+ const selectedSkyboxUrls = resolveSkyboxSelectionUrls(options.skybox, indexSkyboxUrls);
209
+ if (options.skybox === "all") {
210
+ features.add("skybox");
211
+ }
212
+ if (selectedSkyboxUrls?.size) {
213
+ features.add("skybox");
214
+ }
215
+
216
+ const srcDir = join(projectDir, "src");
217
+ let srcContent = "";
218
+ try {
219
+ srcContent = collectSourceFiles(srcDir);
220
+ }
221
+ catch (_e) { }
222
+
223
+ const registerTypesPath = join(projectDir, "src", "generated", "register_types.ts");
224
+ let registerTypes = "";
225
+ try {
226
+ if (existsSync(registerTypesPath)) {
227
+ registerTypes = readFileSync(registerTypesPath, "utf-8");
228
+ }
229
+ }
230
+ catch (_e) { }
231
+
232
+ const allSrc = srcContent + "\n" + registerTypes + "\n" + indexHtml;
233
+ const sceneAnalysis = cachedAnalysis || analyzeProjectGlbs(projectDir);
234
+ const hasWebXRComponent = sceneAnalysis.hasWebXRComponent;
235
+ const hasVideoPlayerComponent = sceneAnalysis.hasVideoPlayerComponent;
236
+
237
+ if (hasWebXRComponent) {
238
+ features.add("xr");
239
+ }
240
+
241
+ if (/polyhaven\.org/i.test(allSrc)) {
242
+ features.add("polyhaven");
243
+ }
244
+
245
+ if (/raw\.githubusercontent\.com/i.test(allSrc)) {
246
+ features.add("github-content");
247
+ }
248
+
249
+ if (/threejs\.org\/examples\/models/i.test(allSrc)) {
250
+ features.add("threejs-models");
251
+ }
252
+
253
+ if (/cdn\.needle\.tools\/static\/models/i.test(allSrc)) {
254
+ features.add("needle-models");
255
+ }
256
+
257
+ if (/cdn\.needle\.tools\/static\/avatars/i.test(allSrc) || /\bAvatarModel\b|\bAvatarLoader\b/i.test(allSrc) || hasWebXRComponent) {
258
+ features.add("needle-avatars");
259
+ }
260
+
261
+ if (/materialx|MaterialXLoader/i.test(allSrc)) {
262
+ features.add("materialx");
263
+ }
264
+ if (Array.from(sceneAnalysis.extensions).some(ext => /materialx/i.test(ext))) {
265
+ features.add("materialx");
266
+ }
267
+
268
+ if (selectedSkyboxUrls && selectedSkyboxUrls.size > 0) {
269
+ features.add("skybox");
270
+ }
271
+
272
+ return {
273
+ features,
274
+ hasWebXR: hasWebXRComponent,
275
+ hasVideoPlayer: hasVideoPlayerComponent,
276
+ allowedSkyboxUrls: selectedSkyboxUrls,
277
+ selectedWebXRProfiles: getWebXRProfilesForMode(options.webxr),
278
+ };
279
+ }
280
+
281
+ /**
282
+ * @param {string} projectDir
283
+ * @returns {SceneAnalysisReport}
284
+ */
285
+ export function analyzeProjectGlbs(projectDir) {
286
+ const sceneFiles = collectAssetSceneFiles(projectDir);
287
+
288
+ const extensions = /** @type {Set<string>} */ (new Set());
289
+ const componentTypes = /** @type {Set<string>} */ (new Set());
290
+ const componentCounts = /** @type {Map<string, number>} */ (new Map());
291
+ const needleExtensionBlobs = /** @type {unknown[]} */ ([]);
292
+
293
+ let hasWebXRComponent = false;
294
+ let hasVideoPlayerComponent = false;
295
+
296
+ let totalNodeCount = 0;
297
+ let totalVertexCount = 0;
298
+ let totalTextureCount = 0;
299
+ let totalMeshCount = 0;
300
+ let totalPrimitiveCount = 0;
301
+ let dracoPrimitiveCount = 0;
302
+ let meshoptBufferViewCount = 0;
303
+
304
+ for (const file of sceneFiles) {
305
+ try {
306
+ const json = file.type === "glb"
307
+ ? readGlbJsonChunk(file.path)
308
+ : readGltfJsonFile(file.path);
309
+ if (!json || typeof json !== "object") continue;
310
+
311
+ const used = Array.isArray(json.extensionsUsed) ? json.extensionsUsed : [];
312
+ for (const ext of used) {
313
+ if (typeof ext === "string") extensions.add(ext);
314
+ }
315
+ if (json.extensions && typeof json.extensions === "object") {
316
+ for (const ext of Object.keys(json.extensions)) {
317
+ extensions.add(ext);
318
+ }
319
+ }
320
+
321
+ totalNodeCount += Array.isArray(json.nodes) ? json.nodes.length : 0;
322
+ totalTextureCount += Array.isArray(json.textures) ? json.textures.length : 0;
323
+
324
+ if (Array.isArray(json.bufferViews)) {
325
+ for (const view of json.bufferViews) {
326
+ const hasMeshopt = !!view?.extensions?.EXT_meshopt_compression;
327
+ if (hasMeshopt) meshoptBufferViewCount++;
328
+ }
329
+ }
330
+
331
+ const accessors = Array.isArray(json.accessors) ? json.accessors : [];
332
+ const meshes = Array.isArray(json.meshes) ? json.meshes : [];
333
+ totalMeshCount += meshes.length;
334
+
335
+ for (const mesh of meshes) {
336
+ const primitives = Array.isArray(mesh?.primitives) ? mesh.primitives : [];
337
+ totalPrimitiveCount += primitives.length;
338
+
339
+ for (const primitive of primitives) {
340
+ const hasDraco = !!primitive?.extensions?.KHR_draco_mesh_compression;
341
+ if (hasDraco) dracoPrimitiveCount++;
342
+
343
+ const positionAccessorIndex = getPrimitivePositionAccessorIndex(primitive);
344
+ if (positionAccessorIndex < 0) continue;
345
+
346
+ const accessor = accessors[positionAccessorIndex];
347
+ if (!accessor || typeof accessor.count !== "number") continue;
348
+ totalVertexCount += accessor.count;
349
+ }
350
+ }
351
+
352
+ const componentNames = collectNeedleComponentNames(json);
353
+ needleExtensionBlobs.push(...collectNeedleComponentExtensionBlobs(json));
354
+ for (const component of componentNames) {
355
+ componentTypes.add(component);
356
+ componentCounts.set(component, (componentCounts.get(component) || 0) + 1);
357
+ if (!hasWebXRComponent && /\bWebXR\b|\bXRRig\b|\bWebARSessionRoot\b/i.test(component)) {
358
+ hasWebXRComponent = true;
359
+ }
360
+ if (!hasVideoPlayerComponent && /\bVideoPlayer\b/i.test(component)) {
361
+ hasVideoPlayerComponent = true;
362
+ }
363
+ }
364
+
365
+ if ((!hasWebXRComponent || !hasVideoPlayerComponent) && componentNames.length > 0) {
366
+ const blob = componentNames.join(" ");
367
+ if (!hasWebXRComponent && /\bWebXR\b|\bXRRig\b|\bWebARSessionRoot\b/i.test(blob)) {
368
+ hasWebXRComponent = true;
369
+ }
370
+ if (!hasVideoPlayerComponent && /\bVideoPlayer\b/i.test(blob)) {
371
+ hasVideoPlayerComponent = true;
372
+ }
373
+ }
374
+ }
375
+ catch (_e) {
376
+ }
377
+ }
378
+
379
+ if (!hasWebXRComponent || !hasVideoPlayerComponent) {
380
+ for (const blob of needleExtensionBlobs) {
381
+ if (!hasWebXRComponent && /\bWebXR\b|\bXRRig\b|\bWebARSessionRoot\b/i.test(blob)) {
382
+ hasWebXRComponent = true;
383
+ }
384
+ if (!hasVideoPlayerComponent && /\bVideoPlayer\b/i.test(blob)) {
385
+ hasVideoPlayerComponent = true;
386
+ }
387
+ if (hasWebXRComponent && hasVideoPlayerComponent) break;
388
+ }
389
+ }
390
+
391
+ return {
392
+ hasWebXRComponent,
393
+ hasVideoPlayerComponent,
394
+ extensions,
395
+ componentTypes,
396
+ componentCounts,
397
+ glbFileCount: sceneFiles.filter(f => f.type === "glb").length,
398
+ gltfFileCount: sceneFiles.filter(f => f.type === "gltf").length,
399
+ totalSceneFileCount: sceneFiles.length,
400
+ totalNodeCount,
401
+ totalVertexCount,
402
+ totalTextureCount,
403
+ totalMeshCount,
404
+ totalPrimitiveCount,
405
+ dracoPrimitiveCount,
406
+ meshoptBufferViewCount,
407
+ };
408
+ }
409
+
410
+ /**
411
+ * @param {Record<string, unknown> | null | undefined} primitive
412
+ * @returns {number}
413
+ */
414
+ function getPrimitivePositionAccessorIndex(primitive) {
415
+ const direct = primitive?.attributes?.POSITION;
416
+ if (typeof direct === "number") return direct;
417
+
418
+ const draco = primitive?.extensions?.KHR_draco_mesh_compression?.attributes?.POSITION;
419
+ if (typeof draco === "number") return draco;
420
+
421
+ const fallback = firstNumericValue(primitive?.attributes);
422
+ if (fallback >= 0) return fallback;
423
+
424
+ const dracoFallback = firstNumericValue(primitive?.extensions?.KHR_draco_mesh_compression?.attributes);
425
+ if (dracoFallback >= 0) return dracoFallback;
426
+
427
+ return -1;
428
+ }
429
+
430
+ /**
431
+ * @param {Record<string, unknown> | null | undefined} obj
432
+ * @returns {number}
433
+ */
434
+ function firstNumericValue(obj) {
435
+ if (!obj || typeof obj !== "object") return -1;
436
+ const values = Object.values(obj);
437
+ for (const value of values) {
438
+ if (typeof value === "number") return value;
439
+ }
440
+ return -1;
441
+ }
442
+
443
+ /**
444
+ * @param {unknown} json
445
+ * @returns {string[]}
446
+ */
447
+ function collectNeedleComponentNames(json) {
448
+ const names = new Set();
449
+
450
+ /** @param {unknown} node */
451
+ function visit(node) {
452
+ if (!node || typeof node !== "object") return;
453
+ if (Array.isArray(node)) {
454
+ for (const item of node) visit(item);
455
+ return;
456
+ }
457
+
458
+ for (const [key, value] of Object.entries(node)) {
459
+ if (key === NEEDLE_COMPONENTS_EXTENSION && value) {
460
+ collectComponentNamesFromNeedleExtension(value, names);
461
+ }
462
+ visit(value);
463
+ }
464
+ }
465
+
466
+ visit(json);
467
+ return Array.from(names);
468
+ }
469
+
470
+ /**
471
+ * @param {import('./local-files-types.js').NeedleComponentExtension | null | undefined} value
472
+ * @param {Set<string>} names
473
+ */
474
+ function collectComponentNamesFromNeedleExtension(value, names) {
475
+ const builtinComponents = Array.isArray(value?.builtin_components)
476
+ ? value.builtin_components
477
+ : [];
478
+
479
+ for (const entry of builtinComponents) {
480
+ const candidateName = getBuiltinComponentName(entry);
481
+ if (candidateName) names.add(candidateName);
482
+ }
483
+ }
484
+
485
+ /**
486
+ * @param {import('./local-files-types.js').NeedleComponentEntry | null | undefined} node
487
+ * @returns {string}
488
+ */
489
+ function getBuiltinComponentName(node) {
490
+ if (!node || typeof node !== "object") return "";
491
+ if (typeof node.name === "string" && node.name.trim()) return node.name.trim();
492
+ return "";
493
+ }
494
+
495
+ /**
496
+ * @param {SceneAnalysisReport} report
497
+ * @param {AutoPolicy | null} autoPolicy
498
+ * @param {string} projectDir
499
+ * @param {string} mode
500
+ * @returns {string}
501
+ */
502
+ function formatSceneAnalysisReport(report, autoPolicy, projectDir, mode) {
503
+ const supportsColor = needleSupportsColor();
504
+ const projectName = relative(process.cwd(), projectDir) || ".";
505
+ const componentList = report.componentCounts?.size > 0
506
+ ? Array.from(report.componentCounts.entries())
507
+ .sort((a, b) => String(a[0]).localeCompare(String(b[0])))
508
+ .map(([name, count]) => {
509
+ const label = String(name);
510
+ if (!count || count <= 1) return label;
511
+ const suffix = ` ${count}×`;
512
+ return supportsColor ? `${label}${needleDim(suffix)}` : `${label}${suffix}`;
513
+ })
514
+ .join(", ")
515
+ : "(none)";
516
+ const extensionList = report.extensions.size > 0
517
+ ? Array.from(report.extensions).sort((a, b) => a.localeCompare(b)).join(", ")
518
+ : "(none)";
519
+ const autoFeatures = autoPolicy?.features?.size
520
+ ? Array.from(autoPolicy.features).sort((a, b) => a.localeCompare(b)).join(", ")
521
+ : "(n/a)";
522
+
523
+ const key = (text) => supportsColor ? needleBlue(text) : text;
524
+
525
+ const lines = [
526
+ "Scene analysis report",
527
+ key("Project") + " : " + projectName,
528
+ key("Scene files") + " : " + report.totalSceneFileCount + " (" + report.glbFileCount + " .glb, " + report.gltfFileCount + " .gltf)",
529
+ key("Nodes") + " : " + report.totalNodeCount,
530
+ key("Meshes / Primitives") + " : " + report.totalMeshCount + " / " + report.totalPrimitiveCount,
531
+ key("Vertices") + " : " + report.totalVertexCount,
532
+ key("Textures") + " : " + report.totalTextureCount,
533
+ key("Compression usage") + " : Draco primitives=" + report.dracoPrimitiveCount + ", Meshopt bufferViews=" + report.meshoptBufferViewCount,
534
+ key("Components") + " : " + componentList,
535
+ key("Extensions") + " : " + extensionList,
536
+ key("Detected Features") + " : " + autoFeatures,
537
+ ];
538
+
539
+ return lines.join("\n");
540
+ }
541
+
542
+ /**
543
+ * @param {string} projectDir
544
+ * @returns {string[]}
545
+ */
546
+ export function collectAssetGlbs(projectDir) {
547
+ const files = collectAssetSceneFiles(projectDir);
548
+ return files.filter(file => file.type === "glb").map(file => file.path);
549
+ }
550
+
551
+ /**
552
+ * @param {string} projectDir
553
+ * @returns {SceneFile[]}
554
+ */
555
+ function collectAssetSceneFiles(projectDir) {
556
+ const assetsDir = join(projectDir, "assets");
557
+ if (!existsSync(assetsDir)) return [];
558
+
559
+ /** @type {SceneFile[]} */
560
+ const out = [];
561
+
562
+ /** @param {string} dir */
563
+ function walk(dir) {
564
+ const entries = readdirSync(dir, { withFileTypes: true });
565
+ for (const entry of entries) {
566
+ const fullPath = join(dir, entry.name);
567
+ if (entry.isDirectory()) {
568
+ walk(fullPath);
569
+ continue;
570
+ }
571
+
572
+ if (/^image_\d+_.*\.glb$/i.test(entry.name)) continue;
573
+ if (/^mesh_lod_\d+_.*\.glb$/i.test(entry.name)) continue;
574
+
575
+ if (/\.glb$/i.test(entry.name)) {
576
+ out.push({ path: fullPath, type: "glb" });
577
+ continue;
578
+ }
579
+ if (/\.gltf$/i.test(entry.name)) {
580
+ out.push({ path: fullPath, type: "gltf" });
581
+ continue;
582
+ }
583
+ }
584
+ }
585
+
586
+ walk(assetsDir);
587
+ return out;
588
+ }
589
+
590
+ /**
591
+ * @param {string} filePath
592
+ * @returns {unknown}
593
+ */
594
+ function readGltfJsonFile(filePath) {
595
+ const text = readFileSync(filePath, "utf8");
596
+ return JSON.parse(text);
597
+ }
598
+
599
+ /**
600
+ * @param {string} filePath
601
+ * @returns {Record<string, unknown>}
602
+ */
603
+ export function readGlbJsonChunk(filePath) {
604
+ const fd = openSync(filePath, "r");
605
+ try {
606
+ const header = Buffer.allocUnsafe(20);
607
+ const bytesRead = readSync(fd, header, 0, header.length, 0);
608
+ if (bytesRead < 20) throw new Error("Invalid GLB header: " + filePath);
609
+
610
+ const magic = header.readUInt32LE(0);
611
+ const version = header.readUInt32LE(4);
612
+ const chunkLength = header.readUInt32LE(12);
613
+ const chunkType = header.readUInt32LE(16);
614
+
615
+ if (magic !== 0x46546c67 || version !== 2) throw new Error("Not a GLB v2: " + filePath);
616
+ if (chunkType !== 0x4E4F534A) throw new Error("First GLB chunk is not JSON: " + filePath);
617
+
618
+ const jsonBuffer = Buffer.allocUnsafe(chunkLength);
619
+ const jsonBytesRead = readSync(fd, jsonBuffer, 0, chunkLength, 20);
620
+ if (jsonBytesRead < chunkLength) throw new Error("Failed to read GLB JSON chunk: " + filePath);
621
+
622
+ const jsonText = jsonBuffer.toString("utf8").replace(/\u0000+$/g, "");
623
+ return JSON.parse(jsonText);
624
+ }
625
+ finally {
626
+ closeSync(fd);
627
+ }
628
+ }
629
+
630
+ /**
631
+ * @param {unknown} json
632
+ * @returns {string[]}
633
+ */
634
+ export function collectNeedleComponentExtensionBlobs(json) {
635
+ const blobs = /** @type {string[]} */ ([]);
636
+
637
+ /** @param {unknown} node */
638
+ function visit(node) {
639
+ if (!node || typeof node !== "object") return;
640
+ if (Array.isArray(node)) {
641
+ for (const item of node) visit(item);
642
+ return;
643
+ }
644
+
645
+ for (const [key, value] of Object.entries(node)) {
646
+ if (key === NEEDLE_COMPONENTS_EXTENSION && value) {
647
+ try {
648
+ blobs.push(JSON.stringify(value));
649
+ }
650
+ catch (_e) { }
651
+ }
652
+ visit(value);
653
+ }
654
+ }
655
+
656
+ visit(json);
657
+ return blobs;
658
+ }
659
+
660
+ /**
661
+ * @param {string} indexHtml
662
+ * @returns {Set<string>}
663
+ */
664
+ export function collectIndexHtmlSkyboxUrls(indexHtml) {
665
+ const urls = new Set();
666
+ if (!indexHtml) return urls;
667
+
668
+ const attrRegex = /\b(background-image|environment-image)\s*=\s*["']([^"']+)["']/gi;
669
+ let match;
670
+ while ((match = attrRegex.exec(indexHtml)) !== null) {
671
+ const value = (match[2] || "").trim();
672
+ const resolved = resolveSkyboxValueToUrl(value);
673
+ if (resolved) urls.add(resolved);
674
+ }
675
+ return urls;
676
+ }
677
+
678
+ /**
679
+ * @param {string | string[] | undefined | null} skyboxOption
680
+ * @param {Set<string>} indexSkyboxUrls
681
+ * @returns {Set<string> | null}
682
+ */
683
+ export function resolveSkyboxSelectionUrls(skyboxOption, indexSkyboxUrls) {
684
+ if (skyboxOption === "all") return null;
685
+
686
+ if (Array.isArray(skyboxOption)) {
687
+ const urls = new Set();
688
+ for (const entry of skyboxOption) {
689
+ const resolved = resolveSkyboxValueToUrl(entry);
690
+ if (resolved) urls.add(resolved);
691
+ }
692
+ return urls;
693
+ }
694
+
695
+ return indexSkyboxUrls;
696
+ }
697
+
698
+ /**
699
+ * @param {string | null | undefined} value
700
+ * @returns {string | null}
701
+ */
702
+ export function resolveSkyboxValueToUrl(value) {
703
+ if (!value) return null;
704
+ if (/^https?:\/\//i.test(value)) return value;
705
+
706
+ const normalized = value.replace(/^\/+/, "");
707
+ if (/\.ktx2$/i.test(normalized)) return SKYBOX_BASE_URL + normalized;
708
+
709
+ if (/^[a-z0-9\-]+$/i.test(normalized)) {
710
+ return SKYBOX_BASE_URL + normalized + ".ktx2";
711
+ }
712
+ return null;
713
+ }
714
+
715
+ /**
716
+ * @param {string | undefined} mode
717
+ * @returns {string[]}
718
+ */
719
+ export function getWebXRProfilesForMode(mode) {
720
+ const allProfiles = [
721
+ "generic-hand",
722
+ "generic-trigger",
723
+ "oculus-touch-v2",
724
+ "oculus-touch-v3",
725
+ "meta-quest-touch-pro",
726
+ "pico-4",
727
+ "pico-neo3",
728
+ ];
729
+
730
+ if (mode === "minimal") return ["generic-hand", "generic-trigger"];
731
+ if (mode === "quest") return ["generic-hand", "generic-trigger", "oculus-touch-v2", "oculus-touch-v3", "meta-quest-touch-pro"];
732
+ if (mode === "pico") return ["generic-hand", "generic-trigger", "pico-4", "pico-neo3"];
733
+ return allProfiles;
734
+ }
735
+
736
+ /**
737
+ * @param {string} dir
738
+ * @param {number} [depth]
739
+ * @returns {string}
740
+ */
741
+ function collectSourceFiles(dir, depth = 0) {
742
+ if (depth > 5) return "";
743
+ if (!existsSync(dir)) return "";
744
+
745
+ let content = "";
746
+ const entries = readdirSync(dir, { withFileTypes: true });
747
+
748
+ for (const entry of entries) {
749
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
750
+
751
+ const fullPath = join(dir, entry.name);
752
+ if (entry.isDirectory()) {
753
+ content += collectSourceFiles(fullPath, depth + 1);
754
+ }
755
+ else if (/\.(ts|js|tsx|jsx|html|vue|svelte)$/i.test(entry.name)) {
756
+ try {
757
+ content += readFileSync(fullPath, "utf-8") + "\n";
758
+ }
759
+ catch (_e) { }
760
+ }
761
+ }
762
+ return content;
763
+ }
764
+
765
+ /**
766
+ * @param {string} url
767
+ * @param {UrlHandler} handler
768
+ * @param {{ options: LocalizationOptions, autoPolicy: AutoPolicy | null }} context
769
+ * @returns {boolean}
770
+ */
771
+ export function shouldHandleUrlInAutoMode(url, handler, context) {
772
+ if (!hasAutoFeatureSelection(context.options.features) || !context.autoPolicy) return true;
773
+
774
+ if (handler.feature === "cdn-scripts" && url.includes(HLS_CDN_SEGMENT)) {
775
+ return context.autoPolicy.hasVideoPlayer;
776
+ }
777
+
778
+ if (handler.feature === "needle-avatars" && /\/static\/avatars\/default/i.test(url) && !context.autoPolicy.hasWebXR) {
779
+ return false;
780
+ }
781
+
782
+ if (handler.feature === "skybox") {
783
+ const allowed = context.autoPolicy.allowedSkyboxUrls;
784
+ if (!allowed) return true;
785
+ return allowed.has(url);
786
+ }
787
+
788
+ return true;
789
+ }