@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
@@ -1,3 +1,4 @@
1
+ // @ts-check
1
2
  import { createWriteStream, existsSync, mkdirSync, readdirSync, rmSync, statSync, write } from "fs";
2
3
 
3
4
  const filename_timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
@@ -15,57 +16,209 @@ let originalConsoleWarn = console.warn;
15
16
  let originalConsoleInfo = console.info;
16
17
  let originalConsoleDebug = console.debug;
17
18
  let didPatch = false;
19
+ /** @type {(() => void) | null} */
18
20
  let unpatchFunction = null;
19
21
 
20
- export function patchConsoleLogs() {
22
+ const buildOutputState = {
23
+ mode: "serve",
24
+ progressActive: false,
25
+ assetFiles: 0,
26
+ assetTotalKb: 0,
27
+ assetGzipKb: 0,
28
+ compressionFiles: 0,
29
+ compressionTotalKb: 0,
30
+ compressionGzipKb: 0,
31
+ compressionCaptureActive: false,
32
+ };
33
+
34
+ /** @param {unknown} value @returns {number} */
35
+ function parseKb(value) {
36
+ const numeric = Number.parseFloat(String(value ?? "0").replace(/[^0-9.]/g, ""));
37
+ return Number.isFinite(numeric) ? numeric : 0;
38
+ }
39
+
40
+ function clearBuildProgressLine() {
41
+ if (!process.stdout.isTTY || !buildOutputState.progressActive) return;
42
+ process.stdout.write("\r\x1b[2K");
43
+ buildOutputState.progressActive = false;
44
+ }
45
+
46
+ /** @param {string} text */
47
+ function writeBuildProgressLine(text) {
48
+ if (!process.stdout.isTTY) return;
49
+ const maxLength = Math.max(24, (process.stdout.columns || 120) - 1);
50
+ const line = text.length > maxLength ? `${text.slice(0, Math.max(0, maxLength - 1))}…` : text;
51
+ process.stdout.write(`\r\x1b[2K${line}\x1b[0K`);
52
+ buildOutputState.progressActive = true;
53
+ }
54
+
55
+ function resetBuildOutputState() {
56
+ buildOutputState.progressActive = false;
57
+ buildOutputState.assetFiles = 0;
58
+ buildOutputState.assetTotalKb = 0;
59
+ buildOutputState.assetGzipKb = 0;
60
+ buildOutputState.compressionFiles = 0;
61
+ buildOutputState.compressionTotalKb = 0;
62
+ buildOutputState.compressionGzipKb = 0;
63
+ buildOutputState.compressionCaptureActive = false;
64
+ }
65
+
66
+ function flushBuildOutputSummaries() {
67
+ if (buildOutputState.mode !== "build") return;
68
+ clearBuildProgressLine();
69
+
70
+ if (buildOutputState.assetFiles > 0) {
71
+ const msg = formatBuildInfoSummaryMessage(`✓ Bundled ${buildOutputState.assetFiles} files (${buildOutputState.assetTotalKb.toFixed(2)} kB${buildOutputState.assetGzipKb > 0 ? `, gzip ${buildOutputState.assetGzipKb.toFixed(2)} kB` : ""})`);
72
+ originalConsoleLog(msg);
73
+ captureLogMessage("server", "log", msg, null);
74
+ }
75
+
76
+ if (buildOutputState.compressionFiles > 0) {
77
+ const msg = formatBuildInfoSummaryMessage(`✓ Gzip compressed ${buildOutputState.compressionFiles} files (${buildOutputState.compressionTotalKb.toFixed(2)} kB → ${buildOutputState.compressionGzipKb.toFixed(2)} kB)`);
78
+ originalConsoleLog(msg);
79
+ captureLogMessage("server", "log", msg, null);
80
+ }
81
+
82
+ resetBuildOutputState();
83
+ }
84
+
85
+ function supportsColorOutput() {
86
+ return !!process.stdout?.isTTY && process.env.NO_COLOR !== "1";
87
+ }
88
+
89
+ /** @param {string} name @returns {string} */
90
+ function formatNeedleHeader(name) {
91
+ if (!supportsColorOutput()) return `[${name}]`;
92
+ if (name.startsWith("needle-")) {
93
+ const suffix = name.substring("needle-".length);
94
+ return `\x1b[32m[\x1b[0m\x1b[32mneedle-\x1b[0m\x1b[1;32m${suffix}\x1b[0m\x1b[32m]\x1b[0m`;
95
+ }
96
+ if (name.startsWith("needle:")) {
97
+ const suffix = name.substring("needle:".length);
98
+ return `\x1b[32m[\x1b[0m\x1b[32mneedle:\x1b[0m\x1b[1;32m${suffix}\x1b[0m\x1b[32m]\x1b[0m`;
99
+ }
100
+ return `\x1b[32m[\x1b[0m\x1b[1;32m${name}\x1b[0m\x1b[32m]\x1b[0m`;
101
+ }
102
+
103
+ /** @param {string} body @returns {string} */
104
+ function formatBuildInfoSummaryMessage(body) {
105
+ return `${formatNeedleHeader("needle-buildinfo")}\n${body}`;
106
+ }
107
+
108
+ /** @param {unknown[]} args @returns {string} */
109
+ function normalizeConsoleArgs(args) {
110
+ return args.map(arg => typeof arg === "string" ? arg : stringifyLog(arg)).join(" ");
111
+ }
112
+
113
+ /** @param {unknown[]} args @returns {boolean} */
114
+ function tryHandleBuildConsoleOutput(args) {
115
+ if (buildOutputState.mode !== "build") return false;
116
+
117
+ const raw = normalizeConsoleArgs(args);
118
+ const message = raw.replace(/\u001b\[[0-9;]*m/g, "").trim();
119
+ if (!message.length) return true;
120
+
121
+ if (/^transforming\s*\(/i.test(message)) {
122
+ writeBuildProgressLine(`⏳ ${message}`);
123
+ return true;
124
+ }
125
+
126
+ if (message.includes("[vite-plugin-compression]:algorithm=")) {
127
+ buildOutputState.compressionCaptureActive = true;
128
+ return true;
129
+ }
130
+
131
+ const assetMatch = message.match(/^dist\/.*?\s+([0-9.,]+)\s*kB(?:\s*[│|]\s*gzip:\s*([0-9.,]+)\s*kB)?$/i);
132
+ if (assetMatch) {
133
+ buildOutputState.assetFiles++;
134
+ buildOutputState.assetTotalKb += parseKb(assetMatch[1]);
135
+ if (assetMatch[2]) buildOutputState.assetGzipKb += parseKb(assetMatch[2]);
136
+ writeBuildProgressLine(`📦 Bundling assets: ${buildOutputState.assetFiles} files`);
137
+ return true;
138
+ }
139
+
140
+ if (buildOutputState.compressionCaptureActive) {
141
+ const compressionMatch = message.match(/^dist\/.*?\s+([0-9.]+)\s*kb\s*\/\s*gzip:\s*([0-9.]+)\s*kb$/i);
142
+ if (compressionMatch) {
143
+ buildOutputState.compressionFiles++;
144
+ buildOutputState.compressionTotalKb += parseKb(compressionMatch[1]);
145
+ buildOutputState.compressionGzipKb += parseKb(compressionMatch[2]);
146
+ writeBuildProgressLine(`🗜️ Compressing: ${buildOutputState.compressionFiles} files`);
147
+ return true;
148
+ }
149
+ }
150
+
151
+ if (message.startsWith("✓ built in")) {
152
+ flushBuildOutputSummaries();
153
+ return false;
154
+ }
155
+
156
+ flushBuildOutputSummaries();
157
+ return false;
158
+ }
159
+
160
+ /**
161
+ * @param {{command?: string} | undefined} [options]
162
+ * @returns {(() => void) | undefined}
163
+ */
164
+ export function patchConsoleLogs(options = undefined) {
21
165
  if (didPatch) return unpatchFunction;
22
166
  didPatch = true;
167
+ buildOutputState.mode = options?.command === "build" ? "build" : "serve";
168
+ resetBuildOutputState();
23
169
 
24
170
  console.log = (...args) => {
171
+ if (tryHandleBuildConsoleOutput(args)) return;
25
172
  originalConsoleLog(...args);
26
173
  captureLogMessage("server", 'log', args, null);
27
174
  };
28
175
  console.error = (...args) => {
176
+ flushBuildOutputSummaries();
29
177
  originalConsoleError(...args);
30
178
  captureLogMessage("server", 'error', args, null);
31
179
  };
32
180
  console.warn = (...args) => {
181
+ flushBuildOutputSummaries();
33
182
  originalConsoleWarn(...args);
34
183
  captureLogMessage("server", 'warn', args, null);
35
184
  };
36
185
  console.info = (...args) => {
186
+ if (tryHandleBuildConsoleOutput(args)) return;
37
187
  originalConsoleInfo(...args);
38
188
  captureLogMessage("server", 'info', args, null);
39
189
  };
40
190
  console.debug = (...args) => {
191
+ if (tryHandleBuildConsoleOutput(args)) return;
41
192
  originalConsoleDebug(...args);
42
193
  captureLogMessage("server", 'debug', args, null);
43
194
  };
44
195
 
45
196
  // Restore original console methods
46
197
  unpatchFunction = () => {
198
+ flushBuildOutputSummaries();
47
199
  didPatch = false;
48
200
  console.log = originalConsoleLog;
49
201
  console.error = originalConsoleError;
50
202
  console.warn = originalConsoleWarn;
51
203
  console.info = originalConsoleInfo;
52
204
  console.debug = originalConsoleDebug;
205
+ buildOutputState.mode = "serve";
53
206
  }
54
207
  return unpatchFunction;
55
208
  }
56
209
 
57
210
 
58
211
  let isCapturing = false;
59
- /** @type {Set<string>} */
212
+ /** @type {Set<unknown>} */
60
213
  const isCapturingLogMessage = new Set();
61
214
 
62
- /** @type {Array<{ process: ProcessType, key: string, log:any, timestamp:number, connectionId: string | null }>} */
215
+ /** @type {Array<{ process: ProcessType, key: string, log:unknown, timestamp:number, connectionId: string | null }>} */
63
216
  const queue = new Array();
64
217
 
65
218
  /**
66
219
  * @param {ProcessType} process
67
220
  * @param {string} key
68
- * @param {any} log
221
+ * @param {unknown} log
69
222
  * @param {string | null} connectionId - Optional connection ID for client logs.
70
223
  * @param {number} [time] - Optional timestamp, defaults to current time.
71
224
  */
@@ -109,10 +262,10 @@ export function captureLogMessage(process, key, log, connectionId, time = Date.n
109
262
 
110
263
  /**
111
264
  * Stringifies a log message, handling circular references and formatting.
112
- * @param {any} log
113
- * @param {Set<any>} [seen]
265
+ * @param {unknown} log
266
+ * @param {Set<unknown>} [seen]
114
267
  */
115
- function stringifyLog(log, seen = new Set(), depth = 0) {
268
+ function stringifyLog(log, seen = /** @type {Set<unknown>} */ (new Set()), depth = 0) {
116
269
  const isServer = typeof window === "undefined";
117
270
  const stringify_limits = {
118
271
  string: isServer ? 100_000 : 1_000,
@@ -154,8 +307,9 @@ function stringifyLog(log, seen = new Set(), depth = 0) {
154
307
  || log instanceof BigUint64Array
155
308
  || log instanceof Float64Array
156
309
  ) {
157
- seen.add(log);
158
- return stringifyArray(log);
310
+ const logArr = /** @type {ArrayLike<unknown>} */ (/** @type {unknown} */ (log));
311
+ seen.add(logArr);
312
+ return stringifyArray(logArr);
159
313
  }
160
314
  if (typeof log === "object") {
161
315
 
@@ -169,11 +323,12 @@ function stringifyLog(log, seen = new Set(), depth = 0) {
169
323
  return `<Error: ${log.message}\nStack: ${log.stack}>`;
170
324
  }
171
325
 
172
- const keys = Object.keys(log);
326
+ const logObj = /** @type {Record<string, unknown>} */ (log);
327
+ const keys = Object.keys(logObj);
173
328
  let res = "{";
174
329
  for (let i = 0; i < keys.length; i++) {
175
330
  const key = keys[i];
176
- let value = log[key];
331
+ let value = logObj[key];
177
332
  if (i >= stringify_limits.object_keys) {
178
333
  res += `, ... <truncated ${keys.length - i} keys>`;
179
334
  break;
@@ -208,6 +363,7 @@ function stringifyLog(log, seen = new Set(), depth = 0) {
208
363
 
209
364
  return String(log);
210
365
 
366
+ /** @param {ArrayLike<unknown>} arr @returns {string} */
211
367
  function stringifyArray(arr) {
212
368
  let res = "";
213
369
  for (let i = 0; i < arr.length; i++) {
@@ -340,6 +496,11 @@ function onExit() {
340
496
  filestreams.forEach((stream) => stream.end());
341
497
  filestreams.clear();
342
498
  }
499
+
500
+ export function closeLogStreams() {
501
+ onExit();
502
+ }
503
+
343
504
  const events = ['SIGTERM', 'SIGINT', 'beforeExit', 'rejectionHandled', 'uncaughtException', 'exit'];
344
505
  for (const event of events) {
345
506
  process.on(event, onExit);
@@ -0,0 +1,175 @@
1
+ ---
2
+ name: needle-engine
3
+ description: Automatically provides Needle Engine context when working in a Needle Engine web project. Use this skill when editing TypeScript components, Vite config, GLB assets, or anything related to @needle-tools/engine.
4
+ ---
5
+
6
+ # Needle Engine
7
+
8
+ You are an expert in Needle Engine — a web-first 3D engine built on Three.js with a Unity/Blender-based workflow.
9
+
10
+ ## Key concepts
11
+
12
+ **Needle Engine** ships 3D scenes from Unity (or Blender) as GLB files and renders them in the browser using Three.js. TypeScript components attached to GameObjects in Unity are serialized into the GLB and re-hydrated at runtime in the browser.
13
+
14
+ ### Embedding in HTML
15
+ ```html
16
+ <!-- The <needle-engine> web component creates and manages a 3D context -->
17
+ <needle-engine src="assets/scene.glb"></needle-engine>
18
+ ```
19
+ Access the context programmatically: `document.querySelector("needle-engine").context`
20
+
21
+ ### Component lifecycle (mirrors Unity MonoBehaviour)
22
+ ```ts
23
+ import { Behaviour, serializable, registerType } from "@needle-tools/engine";
24
+
25
+ @registerType
26
+ export class MyComponent extends Behaviour {
27
+ @serializable() myValue: number = 1;
28
+
29
+ awake() {} // called once when instantiated
30
+ start() {} // called once on first frame
31
+ update() {} // called every frame
32
+ onEnable() {}
33
+ onDisable() {}
34
+ onDestroy() {}
35
+ onBeforeRender(_frame: XRFrame | null) {}
36
+ }
37
+ ```
38
+
39
+ ### Serialization
40
+ - `@registerType` — makes the class discoverable by the GLB deserializer
41
+ - `@serializable()` — marks a field for GLB deserialization (primitives)
42
+ - `@serializable(Object3D)` — for Three.js object references
43
+ - `@serializable(Texture)` — for textures (import Texture from "three")
44
+ - `@serializable(RGBAColor)` — for colors
45
+
46
+ ### Accessing the scene
47
+ ```ts
48
+ this.context.scene // THREE.Scene
49
+ this.context.mainCamera // active camera (THREE.Camera)
50
+ this.context.renderer // THREE.WebGLRenderer
51
+ this.context.time.frame // current frame number
52
+ this.context.time.deltaTime // seconds since last frame
53
+ this.gameObject // the THREE.Object3D this component is on
54
+ ```
55
+
56
+ ### Finding components
57
+ ```ts
58
+ this.gameObject.getComponent(MyComponent)
59
+ this.gameObject.getComponentInChildren(MyComponent)
60
+ this.context.scene.getComponentInChildren(MyComponent)
61
+
62
+ // Global search (import as standalone functions from "@needle-tools/engine")
63
+ import { findObjectOfType, findObjectsOfType } from "@needle-tools/engine";
64
+ findObjectOfType(MyComponent, this.context)
65
+ findObjectsOfType(MyComponent, this.context)
66
+ ```
67
+
68
+ ### Input handling
69
+ ```ts
70
+ // Polling
71
+ if (this.context.input.getPointerDown(0)) { /* pointer pressed */ }
72
+ if (this.context.input.getKeyDown("Space")) { /* space pressed */ }
73
+
74
+ // Event-based (NEPointerEvent works across mouse, touch, and XR controllers)
75
+ this.gameObject.addEventListener("pointerdown", (e: NEPointerEvent) => { });
76
+ ```
77
+
78
+ ### Physics & raycasting
79
+ ```ts
80
+ // Default raycasts hit visible geometry — no colliders needed
81
+ // Uses mesh BVH (bounding volume hierarchy) for accelerated raycasting, BVH is generated on a worker
82
+ const hits = this.context.physics.raycast();
83
+
84
+ // Physics-based raycasts (require colliders, uses Rapier physics engine)
85
+ const physicsHits = this.context.physics.raycastPhysics();
86
+ ```
87
+
88
+ ### Networking & multiplayer
89
+ Needle Engine has built-in multiplayer. Add a `SyncedRoom` component to enable networking.
90
+
91
+ - `@syncField()` — automatically syncs a field across all connected clients
92
+ - Primitives (string, number, boolean) sync automatically on change
93
+ - Complex types (arrays/objects) require reassignment to trigger sync: `this.myArray = this.myArray`
94
+ - Key components: `SyncedRoom`, `SyncedTransform`, `PlayerSync`, `Voip`
95
+ - Uses WebSockets + optional WebRTC peer-to-peer connections
96
+
97
+ ### WebXR (VR & AR)
98
+ Needle Engine has built-in WebXR support for VR and AR across Meta Quest, Apple Vision Pro, and mobile AR.
99
+
100
+ - Add the `WebXR` component to enable VR/AR sessions
101
+ - Use `XRRig` to define the user's starting position — the user is parented to the rig during XR sessions
102
+ - Available components: `WebXRImageTracking`, `WebXRPlaneTracking`, `XRControllerModel`, `NeedleXRSession`
103
+
104
+ ## Creating a new project
105
+
106
+ Use `create-needle` to scaffold a new Needle Engine project:
107
+ ```bash
108
+ npm create needle my-app # default Vite template
109
+ npm create needle my-app -t react # React template
110
+ npm create needle my-app -t vue # Vue.js template
111
+ ```
112
+
113
+ Available templates: `vite` (default), `react`, `vue`, `sveltekit`, `svelte`, `nextjs`, `react-three-fiber`.
114
+
115
+ Use `npm create needle --list` to see all available templates.
116
+
117
+ ## Vite plugin system
118
+
119
+ Needle Engine ships a set of Vite plugins via `needlePlugins(command, config, userSettings)`. Custom project plugins go in `vite.config.ts`.
120
+
121
+ ```ts
122
+ import { defineConfig } from "vite";
123
+ import { needlePlugins } from "@needle-tools/engine/vite";
124
+
125
+ export default defineConfig(async ({ command }) => ({
126
+ plugins: [
127
+ ...(await needlePlugins(command, {}, {})),
128
+ ],
129
+ }));
130
+ ```
131
+
132
+ ## Deployment
133
+
134
+ Projects can be deployed to:
135
+ - **Needle Cloud** — official hosting with automatic optimization (`npx needle-cloud deploy`)
136
+ - **Vercel** / **Netlify** — standard web hosting
137
+ - **itch.io** — for games and interactive experiences
138
+ - **Any static host** — Needle Engine projects are standard Vite web apps
139
+
140
+ From Unity, use built-in deployment components (e.g. `DeployToNeedleCloud`, `DeployToNetlify`).
141
+
142
+ ## Progressive loading (`@needle-tools/gltf-progressive`)
143
+
144
+ Needle Engine includes `@needle-tools/gltf-progressive` for progressive streaming of 3D models and textures. It creates a tiny initial file with embedded low-quality proxy geometry, then streams higher-quality LODs on demand. Results in ~90% smaller initial downloads with instant display.
145
+
146
+ Works standalone with any three.js project:
147
+ ```ts
148
+ import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
149
+ import { WebGLRenderer } from "three";
150
+ import { useNeedleProgressive } from "@needle-tools/gltf-progressive";
151
+
152
+ const gltfLoader = new GLTFLoader();
153
+ const renderer = new WebGLRenderer();
154
+
155
+ // Register once — progressive loading happens automatically for all subsequent loads
156
+ useNeedleProgressive(gltfLoader, renderer);
157
+
158
+ gltfLoader.load(url, (gltf) => scene.add(gltf.scene));
159
+ ```
160
+
161
+ In Needle Engine projects, progressive loading is built in and can be configured via the **Compression & LOD Settings** component in Unity.
162
+
163
+ ## Important URLs
164
+ - Docs: https://engine.needle.tools/docs/
165
+ - Samples: https://engine.needle.tools/samples/
166
+ - GitHub: https://github.com/needle-tools/needle-engine-support
167
+ - npm: https://www.npmjs.com/package/@needle-tools/engine
168
+
169
+ ## Searching the documentation
170
+
171
+ Use the `needle_search` MCP tool to find relevant docs, forum posts, and community answers.
172
+
173
+ ## Common gotchas
174
+ - Components must use `@registerType` or they won't be instantiated from GLB (this is handled automatically when exporting from Unity or Blender, but must be added manually for hand-written components)
175
+ - GLB assets are in `assets/`, static files in `include/` or `public/`
@@ -1,3 +1,4 @@
1
+ import { needleLog } from '../vite/logging.js';
1
2
 
2
3
 
3
4
 
@@ -51,12 +52,12 @@ export function rollupFixWorkerImport(opts = { logFail: true }) {
51
52
  regexMatchedWorkerCode = true;
52
53
  // console.log("WORKER?", url)
53
54
  if (url?.startsWith("/")) {
54
- console.log(`[rollup] Rewrite worker import in ${chunk.fileName}`);
55
+ needleLog("rollup", `Rewrite worker import in ${chunk.fileName}`, "log", { leadingNewline: true, dimBody: false });
55
56
  // Make url file-relative
56
57
  const newUrl = url.replace(/^\//, "");
57
58
  // For CORS issues we need to use importScripts: https://linear.app/needle/issue/NE-6572#comment-ea5dc65e
58
59
  const output = `/* new-worker */ new Worker(URL.createObjectURL(new Blob(["import '" + \`\${new URL('./${newUrl}', import.meta.url).toString()}\` + "';"], { type: 'text/javascript' }))`;
59
- console.log("[rollup] Did rewrite worker output to:", output);
60
+ needleLog("rollup", "Did rewrite worker output to: " + output, "log", { leadingNewline: true });
60
61
  return output;
61
62
  // return `new Worker(new URL("./${newUrl}", import.meta.url)`;
62
63
  }
@@ -70,7 +71,7 @@ export function rollupFixWorkerImport(opts = { logFail: true }) {
70
71
  }
71
72
  if (opts?.logFail !== false) {
72
73
  const str = `[...]${code.substring(newWorkerStartIndex, newWorkerStartIndex + 200)}[...]`
73
- console.warn(`\n[rollup] Worker import in ${chunk.fileName} was not rewritten: ${str}`);
74
+ needleLog("rollup", `Worker import in ${chunk.fileName} was not rewritten: ${str}`, "warn", { leadingNewline: true, dimBody: false });
74
75
  }
75
76
  }
76
77
  return res;
@@ -116,7 +117,7 @@ function fixWorkerSelfLocation(filename, code) {
116
117
  const fixedCode = workerCode.replace("self.location", "import.meta.url");
117
118
  code = code.substring(0, startIndex) + fixedCode + code.substring(endIndex + 1);
118
119
  lastIndex = startIndex + fixedCode.length;
119
- console.log(`[rollup] Rewrite worker 'self.location' to 'import.meta.url' in ${filename}`);
120
+ needleLog("rollup", `Rewrite worker 'self.location' to 'import.meta.url' in ${filename}`, "log", { leadingNewline: true, dimBody: false });
120
121
  } else {
121
122
  lastIndex = endIndex;
122
123
  }
@@ -112,10 +112,48 @@ export type userSettings = {
112
112
  debugLicense?: boolean;
113
113
 
114
114
  /**
115
- * Experimental: When enabled then fonts from google fonts will be downloaded and copied to the output directory
115
+ * When enabled, external CDN URLs are downloaded at build time and bundled locally.
116
+ * This creates fully self-contained deployments that work without internet access.
117
+ *
118
+ * - `true` — enable with all features (download everything)
119
+ * - `"auto"` — automatically detect which features the project uses and only include those
120
+ * - `{ enabled: true }` — same as `true`
121
+ * - `{ enabled: true, features: "auto" }` — same as `"auto"`
122
+ * - `{ enabled: true, features: ["draco", "ktx2"] }` — only include specific features
123
+ * - `{ enabled: true, excludeFeatures: ["xr"] }` — include all except specific features
124
+ * - `{ enabled: true, features: "auto", excludeFeatures: ["skybox"] }` — auto-detect but exclude specific features
125
+ *
126
+ * Available features:
127
+ * - `"draco"` — Draco mesh decoders
128
+ * - `"ktx2"` — KTX2/Basis texture transcoders
129
+ * - `"materialx"` — MaterialX WASM shader compiler
130
+ * - `"xr"` — WebXR input profiles (controllers/hands)
131
+ * - `"skybox"` — Skybox/environment textures
132
+ * - `"fonts"` — Google Fonts CSS + font files
133
+ * - `"needle-fonts"` — Needle font assets (MSDF, etc.)
134
+ * - `"needle-models"` — Needle models
135
+ * - `"needle-avatars"` — Needle avatars
136
+ * - `"polyhaven"` — Polyhaven HDRIs/models
137
+ * - `"cdn-scripts"` — Third-party scripts (QRCode.js, vConsole, HLS.js)
138
+ * - `"github-content"` — GitHub raw content files
139
+ * - `"threejs-models"` — three.js example models
140
+ * - `"needle-uploads"` — Needle uploads assets
116
141
  */
117
- makeFilesLocal?: boolean | {
142
+ makeFilesLocal?: boolean | "auto" | {
118
143
  enabled: boolean;
144
+ /** URL patterns to exclude from making local */
145
+ exclude?: string[];
146
+ /** Target platform preset */
147
+ platform?: "discord" | "facebook-instant" | null;
148
+ /**
149
+ * Feature categories to include.
150
+ * - `"auto"` — Automatically detect which features the project uses.
151
+ * - `FeatureName[]` — Explicit list. When set, ONLY these features are processed.
152
+ * - When omitted, ALL features are included.
153
+ */
154
+ features?: "auto" | Array<"draco" | "ktx2" | "materialx" | "xr" | "skybox" | "fonts" | "needle-fonts" | "needle-models" | "needle-avatars" | "polyhaven" | "cdn-scripts" | "github-content" | "threejs-models" | "needle-uploads">;
155
+ /** Feature categories to exclude. Applied after `features` (including after auto-detection). */
156
+ excludeFeatures?: Array<"draco" | "ktx2" | "materialx" | "xr" | "skybox" | "fonts" | "needle-fonts" | "needle-models" | "needle-avatars" | "polyhaven" | "cdn-scripts" | "github-content" | "threejs-models" | "needle-uploads">;
119
157
  }
120
158
 
121
159
  /**
@@ -0,0 +1,71 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
+ import { dirname, join } from "path";
3
+ import { fileURLToPath } from "url";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+
8
+ const pluginName = "needle-ai";
9
+
10
+ /**
11
+ * Needle Engine Claude skill installer.
12
+ *
13
+ * Writes a Needle Engine skill to `.claude/skills/needle-engine/SKILL.md`.
14
+ * Claude Code auto-loads skills based on their description frontmatter, so
15
+ * Claude will automatically have Needle Engine context when working in the project.
16
+ *
17
+ * The skill is only written if `.claude/` already exists in the project root
18
+ * (i.e. the developer is already using Claude Code). Old skill files are
19
+ * always overwritten so the skill stays up to date with the engine version.
20
+ *
21
+ * @param {"build" | "serve"} command
22
+ * @param {{} | undefined | null} config
23
+ * @param {import('../types/index.js').userSettings} userSettings
24
+ * @returns {import('vite').Plugin | null}
25
+ */
26
+ export const needleAI = (command, config, userSettings) => {
27
+ return {
28
+ name: pluginName,
29
+ enforce: "pre",
30
+ buildStart() {
31
+ installClaudeSkill();
32
+ },
33
+ configureServer() {
34
+ installClaudeSkill();
35
+ },
36
+ };
37
+ };
38
+
39
+ /** Read the engine version from our own package.json */
40
+ function getEngineVersion() {
41
+ try {
42
+ const pkgPath = join(__dirname, "../../package.json");
43
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
44
+ return pkg.version || "unknown";
45
+ }
46
+ catch {
47
+ return "unknown";
48
+ }
49
+ }
50
+
51
+ function writeSkill(claudeDir, version) {
52
+ const skillDir = join(claudeDir, "skills", "needle-engine");
53
+ if (!existsSync(skillDir)) {
54
+ mkdirSync(skillDir, { recursive: true });
55
+ }
56
+ const skillPath = join(skillDir, "SKILL.md");
57
+ const templatePath = join(__dirname, "../common/needle-engine-skill.md");
58
+ const template = readFileSync(templatePath, "utf8");
59
+ const content = template.replace("{{VERSION}}", version);
60
+ writeFileSync(skillPath, content, "utf8");
61
+ return skillPath;
62
+ }
63
+
64
+ function installClaudeSkill() {
65
+ const claudeDir = join(process.cwd(), ".claude");
66
+ if (!existsSync(claudeDir)) return; // only install if developer uses Claude Code
67
+
68
+ const version = getEngineVersion();
69
+ const path = writeSkill(claudeDir, version);
70
+ console.log(`[${pluginName}] Installed Needle Engine Claude skill → ${path}`);
71
+ }
@@ -1,5 +1,6 @@
1
1
  import { createWriteStream, existsSync, mkdirSync, rmSync, writeFileSync } from 'fs';
2
2
  import path from 'path';
3
+ import { needleLog } from './logging.js';
3
4
 
4
5
  const projectDir = process.cwd() + "/";
5
6
 
@@ -77,7 +78,7 @@ export const needleViteAlias = (command, config, userSettings) => {
77
78
 
78
79
  let outputDebugFile = null;
79
80
  function log(...msg) {
80
- console.log(...msg);
81
+ needleLog("needle-alias", msg.join(" "));
81
82
  if (debug) logToFile(...msg);
82
83
  }
83
84
  function logToFile(...msg) {
@@ -91,7 +92,7 @@ export const needleViteAlias = (command, config, userSettings) => {
91
92
  const timestamp = new Date().toISOString();
92
93
  outputDebugFile.write("\n\n\n--------------------------\n");
93
94
  outputDebugFile.write(`[needle-alias] Logging to: ${outputFilePath} (${timestamp})\n`);
94
- log("[needle-alias] Logging to: ", outputFilePath);
95
+ log("Logging to:", outputFilePath);
95
96
  }
96
97
 
97
98
 
@@ -99,7 +100,7 @@ export const needleViteAlias = (command, config, userSettings) => {
99
100
  const aliasPlugin = {
100
101
  name: "needle-alias",
101
102
  config(config) {
102
- if (debug) console.log('[needle-alias] ProjectDirectory: ' + projectDir);
103
+ if (debug) log('ProjectDirectory: ' + projectDir);
103
104
  if (!config.resolve) config.resolve = {};
104
105
  if (!config.resolve.alias) config.resolve.alias = {};
105
106
  const aliasDict = config.resolve.alias;
@@ -119,7 +120,7 @@ export const needleViteAlias = (command, config, userSettings) => {
119
120
  config.optimizeDeps.exclude ??= [];
120
121
  if (!config.optimizeDeps.include?.includes('@needle-tools/engine')) {
121
122
  config.optimizeDeps.exclude.push('@needle-tools/engine');
122
- log("[needle-alias] Detected local @needle-tools/engine package → will exclude it from optimization");
123
+ log("Detected local @needle-tools/engine package → will exclude it from optimization");
123
124
  }
124
125
  }
125
126
 
@@ -132,7 +133,7 @@ export const needleViteAlias = (command, config, userSettings) => {
132
133
  if (typeof entry === "function") res = entry(name, 0, name);
133
134
  testResults.push({ name, entry: res });
134
135
  }
135
- console.log('[needle-alias] Aliases: ', testResults);
136
+ log('Aliases: ' + JSON.stringify(testResults));
136
137
  }
137
138
  },
138
139
  }