@needle-tools/engine 5.1.0-canary.db0c38f → 5.1.0-canary.e9062c0

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 (82) hide show
  1. package/.needle/generated/needle-bindings.gen.d.ts +5 -0
  2. package/components.needle.json +1 -1
  3. package/dist/{needle-engine.bundle-Dq0Ly8fW.umd.cjs → needle-engine.bundle--q_w7iXI.umd.cjs} +125 -125
  4. package/dist/{needle-engine.bundle-B29kieh0.js → needle-engine.bundle-CDKcuALa.js} +3481 -3422
  5. package/dist/{needle-engine.bundle-YnpzzOPL.min.js → needle-engine.bundle-w22UkaT_.min.js} +137 -137
  6. package/dist/needle-engine.d.ts +61 -41
  7. package/dist/needle-engine.js +2 -2
  8. package/dist/needle-engine.min.js +1 -1
  9. package/dist/needle-engine.umd.cjs +1 -1
  10. package/lib/engine/api.d.ts +1 -1
  11. package/lib/engine/engine_context.d.ts +7 -3
  12. package/lib/engine/engine_context.js +16 -2
  13. package/lib/engine/engine_context.js.map +1 -1
  14. package/lib/engine/engine_init.js +5 -0
  15. package/lib/engine/engine_init.js.map +1 -1
  16. package/lib/engine/engine_license.d.ts +2 -0
  17. package/lib/engine/engine_license.js +14 -6
  18. package/lib/engine/engine_license.js.map +1 -1
  19. package/lib/engine/engine_lifecycle_functions_internal.js +5 -0
  20. package/lib/engine/engine_lifecycle_functions_internal.js.map +1 -1
  21. package/lib/engine/engine_pmrem.js +2 -2
  22. package/lib/engine/engine_pmrem.js.map +1 -1
  23. package/lib/engine/engine_scenedata.d.ts +8 -10
  24. package/lib/engine/engine_scenedata.js +44 -20
  25. package/lib/engine/engine_scenedata.js.map +1 -1
  26. package/lib/engine/engine_ssr.d.ts +2 -0
  27. package/lib/engine/engine_ssr.js +20 -0
  28. package/lib/engine/engine_ssr.js.map +1 -1
  29. package/lib/engine/engine_types.d.ts +2 -0
  30. package/lib/engine/engine_types.js.map +1 -1
  31. package/lib/engine/webcomponents/jsx.d.ts +44 -0
  32. package/lib/engine/webcomponents/logo-element.js.map +1 -1
  33. package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +2 -3
  34. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  35. package/lib/engine/webcomponents/needle-button.js.map +1 -1
  36. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  37. package/lib/engine-components/Light.d.ts +6 -8
  38. package/lib/engine-components/Light.js +38 -27
  39. package/lib/engine-components/Light.js.map +1 -1
  40. package/lib/needle-engine.d.ts +2 -0
  41. package/lib/needle-engine.js +2 -0
  42. package/lib/needle-engine.js.map +1 -1
  43. package/package.json +3 -2
  44. package/plugins/dts-generator/dts.codegen.js +255 -50
  45. package/plugins/dts-generator/dts.scan.js +37 -9
  46. package/plugins/dts-generator/dts.writer.js +1 -1
  47. package/plugins/dts-generator/glb.discovery.js +140 -23
  48. package/plugins/dts-generator/glb.extractor.js +48 -8
  49. package/plugins/dts-generator/glb.reader.js +80 -27
  50. package/plugins/dts-generator/index.js +1 -1
  51. package/plugins/types/needle-bindings.d.ts +25 -14
  52. package/plugins/types/userconfig.d.ts +12 -0
  53. package/plugins/vite/asap.js +1 -1
  54. package/plugins/vite/dependency-watcher.d.ts +2 -2
  55. package/plugins/vite/dependency-watcher.js +3 -4
  56. package/plugins/vite/drop.d.ts +2 -2
  57. package/plugins/vite/drop.js +3 -4
  58. package/plugins/vite/dts-generator.d.ts +2 -2
  59. package/plugins/vite/dts-generator.js +43 -9
  60. package/plugins/vite/index.d.ts +9 -3
  61. package/plugins/vite/index.js +23 -10
  62. package/plugins/vite/meta.js +4 -2
  63. package/plugins/vite/poster.d.ts +2 -2
  64. package/plugins/vite/poster.js +3 -5
  65. package/plugins/vite/reload.d.ts +2 -2
  66. package/plugins/vite/reload.js +4 -5
  67. package/src/engine/api.ts +1 -1
  68. package/src/engine/engine_context.ts +19 -3
  69. package/src/engine/engine_init.ts +4 -0
  70. package/src/engine/engine_license.ts +12 -10
  71. package/src/engine/engine_lifecycle_functions_internal.ts +7 -0
  72. package/src/engine/engine_pmrem.ts +3 -3
  73. package/src/engine/engine_scenedata.ts +41 -18
  74. package/src/engine/engine_ssr.ts +29 -3
  75. package/src/engine/engine_types.ts +2 -0
  76. package/src/engine/webcomponents/jsx.d.ts +44 -0
  77. package/src/engine/webcomponents/logo-element.ts +1 -0
  78. package/src/engine/webcomponents/needle menu/needle-menu.ts +2 -1
  79. package/src/engine/webcomponents/needle-button.ts +1 -0
  80. package/src/engine/webcomponents/needle-engine.ts +1 -0
  81. package/src/engine-components/Light.ts +39 -26
  82. package/src/needle-engine.ts +3 -0
@@ -1,10 +1,10 @@
1
1
  /**
2
- * @param {"build" | "serve"} command
2
+ * @param {"build" | "serve" | undefined} _command
3
3
  * @param {unknown} _config
4
4
  * @param {import('../types').userSettings} userSettings
5
5
  * @returns {import('vite').Plugin | null}
6
6
  */
7
- export function needleDependencyWatcher(command: "build" | "serve", _config: unknown, userSettings: import("../types").userSettings): import("vite").Plugin | null;
7
+ export function needleDependencyWatcher(_command: "build" | "serve" | undefined, _config: unknown, userSettings: import("../types").userSettings): import("vite").Plugin | null;
8
8
  export type PackageJson = {
9
9
  dependencies?: Record<string, string>;
10
10
  devDependencies?: Record<string, string>;
@@ -12,14 +12,12 @@ function log(...msg) {
12
12
  }
13
13
 
14
14
  /**
15
- * @param {"build" | "serve"} command
15
+ * @param {"build" | "serve" | undefined} _command
16
16
  * @param {unknown} _config
17
17
  * @param {import('../types').userSettings} userSettings
18
18
  * @returns {import('vite').Plugin | null}
19
19
  */
20
- export function needleDependencyWatcher(command, _config, userSettings) {
21
- if (command === "build") return null;
22
-
20
+ export function needleDependencyWatcher(_command, _config, userSettings) {
23
21
  if (userSettings?.noDependencyWatcher === true) return null;
24
22
 
25
23
  const dir = process.cwd();
@@ -28,6 +26,7 @@ export function needleDependencyWatcher(command, _config, userSettings) {
28
26
 
29
27
  return /** @type {import('vite').Plugin} */ ({
30
28
  name: 'needle-dependency-watcher',
29
+ apply: 'serve',
31
30
  /** @param {import('vite').ViteDevServer} server */
32
31
  configureServer(server) {
33
32
  manageClients(server);
@@ -1,7 +1,7 @@
1
1
  /** Experimental, allow dropping files from Unity into the running scene.
2
- * @param {"build" | "serve"} command
2
+ * @param {"build" | "serve" | undefined} _command
3
3
  * @param {import('../types/needleConfig').needleMeta | null | undefined} config
4
4
  * @param {import('../types/userconfig.js').userSettings} userSettings
5
5
  * @returns {import('vite').Plugin | null | undefined}
6
6
  */
7
- export function needleDrop(command: "build" | "serve", config: import("../types/needleConfig").needleMeta | null | undefined, userSettings: import("../types/userconfig.js").userSettings): import("vite").Plugin | null | undefined;
7
+ export function needleDrop(_command: "build" | "serve" | undefined, config: import("../types/needleConfig").needleMeta | null | undefined, userSettings: import("../types/userconfig.js").userSettings): import("vite").Plugin | null | undefined;
@@ -7,18 +7,17 @@ const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = path.dirname(__filename);
8
8
 
9
9
  /** Experimental, allow dropping files from Unity into the running scene.
10
- * @param {"build" | "serve"} command
10
+ * @param {"build" | "serve" | undefined} _command
11
11
  * @param {import('../types/needleConfig').needleMeta | null | undefined} config
12
12
  * @param {import('../types/userconfig.js').userSettings} userSettings
13
13
  * @returns {import('vite').Plugin | null | undefined}
14
14
  */
15
- export function needleDrop(command, config, userSettings) {
16
- if (command === "build") return;
17
-
15
+ export function needleDrop(_command, config, userSettings) {
18
16
  if(userSettings.useDrop !== true) return null;
19
17
 
20
18
  return {
21
19
  name: "needle:drop",
20
+ apply: 'serve',
22
21
  config(/** @type {{ server?: { hmr?: { port?: number } } }} */ viteConfig) {
23
22
  if(userSettings)
24
23
  if (!viteConfig.server) viteConfig.server = {};
@@ -2,6 +2,6 @@
2
2
  * @param {"build" | "serve"} _command Vite command (unused — runs in both modes)
3
3
  * @param {import('../types/needleConfig').needleMeta | null | undefined} _config
4
4
  * @param {import('../types').userSettings} [_userSettings]
5
- * @returns {import('vite').Plugin}
5
+ * @returns {import('vite').Plugin | null}
6
6
  */
7
- export function needleDtsGenerator(_command: "build" | "serve", _config: import("../types/needleConfig").needleMeta | null | undefined, _userSettings?: import("../types").userSettings): import("vite").Plugin;
7
+ export function needleDtsGenerator(_command: "build" | "serve", _config: import("../types/needleConfig").needleMeta | null | undefined, _userSettings?: import("../types").userSettings): import("vite").Plugin | null;
@@ -16,12 +16,24 @@
16
16
  * needleDtsGenerator(command, needleConfig, userSettings)
17
17
  */
18
18
 
19
- import { join, resolve } from 'path';
20
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
19
+ import { join, resolve, dirname } from 'path';
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';
23
23
  import { needleLog } from './logging.js';
24
24
 
25
+ // Two dirs up from plugins/vite/ → package root.
26
+ // Use fileURLToPath so %7E and other URL-encoded characters in the path are decoded correctly.
27
+ // realpathSync follows symlinks so the path is stable even in npm link / monorepo setups.
28
+ import { fileURLToPath } from 'url';
29
+ let _packageRoot = /** @type {string | null} */ (null);
30
+ try {
31
+ _packageRoot = realpathSync(join(dirname(fileURLToPath(import.meta.url)), '..', '..'));
32
+ } catch (_e) {
33
+ // If we can't resolve the package root (e.g. unusual install layout), the plugin
34
+ // will silently skip generation rather than crashing the dev server.
35
+ }
36
+
25
37
  /**
26
38
  * Ensure `.vscode/settings.json` references the generated `needle-html-data.json`
27
39
  * so VS Code provides `data-bind-needle` completions in HTML files automatically.
@@ -60,9 +72,14 @@ function ensureVscodeHtmlCustomData(projectRoot, htmlDataPath) {
60
72
  * @param {"build" | "serve"} _command Vite command (unused — runs in both modes)
61
73
  * @param {import('../types/needleConfig').needleMeta | null | undefined} _config
62
74
  * @param {import('../types').userSettings} [_userSettings]
63
- * @returns {import('vite').Plugin}
75
+ * @returns {import('vite').Plugin | null}
64
76
  */
65
77
  export function needleDtsGenerator(_command, _config, _userSettings) {
78
+
79
+ if(_userSettings?.dts?.enabled === false) {
80
+ return null;
81
+ }
82
+
66
83
  let projectRoot = process.cwd();
67
84
 
68
85
  function resolveCodegenDir() {
@@ -73,7 +90,11 @@ export function needleDtsGenerator(_command, _config, _userSettings) {
73
90
  }
74
91
 
75
92
  function resolveOutputPath() {
76
- return join(resolveCodegenDir(), "needle-bindings.d.ts");
93
+ // Write to a .needle dotfolder at the package root.
94
+ // Not in the `files` allowlist → never npm-published.
95
+ // ../../.needle/ from plugins/types/ resolves correctly for both
96
+ // symlinked (js/package~/) and published (node_modules/@needle-tools/engine/) layouts.
97
+ return join(/** @type {string} */ (_packageRoot), ".needle", "generated", "needle-bindings.gen.d.ts");
77
98
  }
78
99
 
79
100
  function resolveAssetsDir() {
@@ -86,14 +107,24 @@ export function needleDtsGenerator(_command, _config, _userSettings) {
86
107
  /** @type {import('vite').ViteDevServer | undefined} */
87
108
  let devServer;
88
109
 
89
- async function run() {
110
+ /** @type {Promise<void> | null} */
111
+ let _runInFlight = null;
112
+
113
+ function run() {
114
+ if (_runInFlight) return _runInFlight;
115
+ _runInFlight = _doRun().finally(() => { _runInFlight = null; });
116
+ return _runInFlight;
117
+ }
118
+
119
+ async function _doRun() {
90
120
  try {
121
+ if (!_packageRoot) return;
91
122
  const assetsDir = resolveAssetsDir();
92
123
  const outputPath = resolveOutputPath();
93
124
  const codegenDir = resolveCodegenDir();
94
125
  const count = await generateBindingsDts({ assetsDir, outputPath, projectRoot, codegenDir });
95
- // Always register HTML custom data — the file is written on every run
96
- const htmlDataPath = join(codegenDir, "needle-html-data.json");
126
+ // HTML custom data sits next to the generated dts in .needle/generated/
127
+ const htmlDataPath = join(/** @type {string} */ (_packageRoot), ".needle", "generated", "needle-html-data.json");
97
128
  ensureVscodeHtmlCustomData(projectRoot, htmlDataPath);
98
129
  if (count !== false) {
99
130
  needleLog("needle:dts-generator", `${count} binding(s) → ${outputPath.replace(process.cwd(), ".")}`);
@@ -102,7 +133,7 @@ export function needleDtsGenerator(_command, _config, _userSettings) {
102
133
  hot.send({ type: "full-reload", path: "*" });
103
134
  }
104
135
  } else {
105
- needleLog("needle:dts-generator", `up-to-date`);
136
+ needleLog("needle:dts-generator", `up-to-date → ${outputPath.replace(process.cwd(), ".")}`);
106
137
  }
107
138
  } catch (err) {
108
139
  needleLog("needle:dts-generator", "Failed: " + (/** @type {any} */ (err)?.message ?? err));
@@ -118,7 +149,10 @@ export function needleDtsGenerator(_command, _config, _userSettings) {
118
149
  },
119
150
 
120
151
  buildStart() {
121
- return run();
152
+ // In serve mode, the configureServer post-hook runs instead.
153
+ // Only run here for actual builds (no devServer).
154
+ if (!devServer) return run();
155
+ return undefined;
122
156
  },
123
157
 
124
158
  /** @param {import('vite').ViteDevServer} server */
@@ -19,12 +19,18 @@
19
19
  * ],
20
20
  * }
21
21
  * ```
22
- * @param {"build" | "serve"} command
23
- * @param {import('../types/needleConfig').needleMeta | null | undefined} config
22
+ * When used without arguments (e.g. in a SvelteKit `defineConfig({})` plain object),
23
+ * the command is resolved automatically from Vite's `configResolved` hook.
24
+ * ```js
25
+ * // SvelteKit — no command needed:
26
+ * export default defineConfig({ plugins: [sveltekit(), needlePlugins()] });
27
+ * ```
28
+ * @param {"build" | "serve" | undefined} [command]
29
+ * @param {import('../types/needleConfig').needleMeta | null | undefined} [config]
24
30
  * @param {import('../types/index.js').userSettings} [userSettings]
25
31
  * @returns {Promise<import('vite').Plugin[]>}
26
32
  */
27
- export function needlePlugins(command: "build" | "serve", config?: import("../types/needleConfig").needleMeta | null | undefined, userSettings?: import("../types/index.js").userSettings): Promise<import("vite").Plugin[]>;
33
+ export function needlePlugins(command?: "build" | "serve" | undefined, config?: import("../types/needleConfig").needleMeta | null | undefined, userSettings?: import("../types/index.js").userSettings): Promise<import("vite").Plugin[]>;
28
34
  export { needleAI } from "./ai.js";
29
35
  export { needleDtsGenerator } from "./dts-generator.js";
30
36
  export { needleAsap } from "./asap.js";
@@ -116,12 +116,18 @@ const defaultUserSettings = {
116
116
  * ],
117
117
  * }
118
118
  * ```
119
- * @param {"build" | "serve"} command
120
- * @param {import('../types/needleConfig').needleMeta | null | undefined} config
119
+ * When used without arguments (e.g. in a SvelteKit `defineConfig({})` plain object),
120
+ * the command is resolved automatically from Vite's `configResolved` hook.
121
+ * ```js
122
+ * // SvelteKit — no command needed:
123
+ * export default defineConfig({ plugins: [sveltekit(), needlePlugins()] });
124
+ * ```
125
+ * @param {"build" | "serve" | undefined} [command]
126
+ * @param {import('../types/needleConfig').needleMeta | null | undefined} [config]
121
127
  * @param {import('../types/index.js').userSettings} [userSettings]
122
128
  * @returns {Promise<import('vite').Plugin[]>}
123
129
  */
124
- export async function needlePlugins(command, config = undefined, userSettings = {}) {
130
+ export async function needlePlugins(command = undefined, config = undefined, userSettings = {}) {
125
131
 
126
132
  if (!config) config = {}
127
133
 
@@ -168,14 +174,21 @@ export async function needlePlugins(command, config = undefined, userSettings =
168
174
  array.push(await editorConnection(command, config, userSettings, array));
169
175
  array.push(needleDependencyWatcher(command, config, userSettings));
170
176
 
171
- // Ensure the process exits on SIGINT (Ctrl+C) since plugin timers/sockets can keep the event loop alive
172
- if (command === "serve") {
177
+ // Ensure the process exits on SIGINT (Ctrl+C) since plugin timers/sockets can keep the event loop alive.
178
+ // If command is unknown at call time (e.g. SvelteKit plain defineConfig), defer to configResolved.
179
+ const registerSigint = () => {
173
180
  process.on('SIGINT', () => {
174
- // Wait a moment to allow plugins to clean up if needed, then exit
175
- setTimeout(() => {
176
- console.debug('\nGoodbye!');
177
- process.exit();
178
- }, 1000);
181
+ setTimeout(() => { console.debug('\nGoodbye!'); process.exit(); }, 1000);
182
+ });
183
+ };
184
+ if (command === "serve") {
185
+ registerSigint();
186
+ } else if (command === undefined) {
187
+ array.push({
188
+ name: "needle:sigint",
189
+ configResolved(config) {
190
+ if (config.command === "serve") registerSigint();
191
+ }
179
192
  });
180
193
  }
181
194
 
@@ -13,8 +13,7 @@ import { needleGreenBold, needleLog } from './logging.js';
13
13
  */
14
14
  export function needleMeta(command, config, userSettings) {
15
15
 
16
- // we can check if this is a build
17
- const isBuild = command === 'build';
16
+ let isBuild = command === 'build';
18
17
 
19
18
  async function updateConfig() {
20
19
  config = await loadConfig();
@@ -25,6 +24,9 @@ export function needleMeta(command, config, userSettings) {
25
24
  return {
26
25
  // replace meta tags
27
26
  name: 'needle:meta',
27
+ configResolved(resolvedConfig) {
28
+ isBuild = resolvedConfig.command === 'build';
29
+ },
28
30
  transformIndexHtml: {
29
31
  order: 'pre',
30
32
  handler(/** @type {string} */ html, /** @type {unknown} */ _ctx) {
@@ -1,8 +1,8 @@
1
1
  export function getPosterPath(): string;
2
2
  /**
3
- * @param {"build" | "serve"} command
3
+ * @param {"build" | "serve" | undefined} _command
4
4
  * @param {import('../types/needleConfig').needleMeta | null | undefined} config
5
5
  * @param {import('../types').userSettings} userSettings
6
6
  * @returns {import('vite').Plugin | undefined}
7
7
  */
8
- export function needlePoster(command: "build" | "serve", config: import("../types/needleConfig").needleMeta | null | undefined, userSettings: import("../types").userSettings): import("vite").Plugin | undefined;
8
+ export function needlePoster(_command: "build" | "serve" | undefined, config: import("../types/needleConfig").needleMeta | null | undefined, userSettings: import("../types").userSettings): import("vite").Plugin | undefined;
@@ -10,19 +10,17 @@ export function getPosterPath() {
10
10
  }
11
11
 
12
12
  /**
13
- * @param {"build" | "serve"} command
13
+ * @param {"build" | "serve" | undefined} _command
14
14
  * @param {import('../types/needleConfig').needleMeta | null | undefined} config
15
15
  * @param {import('../types').userSettings} userSettings
16
16
  * @returns {import('vite').Plugin | undefined}
17
17
  */
18
- export function needlePoster(command, config, userSettings) {
19
- // only relevant for local development
20
- if (command === 'build') return [];
21
-
18
+ export function needlePoster(_command, config, userSettings) {
22
19
  if (userSettings.noPoster) return;
23
20
 
24
21
  return {
25
22
  name: 'needle:poster',
23
+ apply: 'serve',
26
24
  configureServer(server) {
27
25
  const hot = server.hot ?? server.ws;
28
26
  hot.on('needle:screenshot', async (data, client) => {
@@ -1,7 +1,7 @@
1
1
  /**
2
- * @param {"build" | "serve"} command
2
+ * @param {"build" | "serve" | undefined} _command
3
3
  * @param {import('../types/needleConfig').needleMeta | null} config
4
4
  * @param {import('../types').userSettings} userSettings
5
5
  * @returns {import('vite').Plugin | undefined}
6
6
  */
7
- export function needleReload(command: "build" | "serve", config: import("../types/needleConfig").needleMeta | null, userSettings: import("../types").userSettings): import("vite").Plugin | undefined;
7
+ export function needleReload(_command: "build" | "serve" | undefined, config: import("../types/needleConfig").needleMeta | null, userSettings: import("../types").userSettings): import("vite").Plugin | undefined;
@@ -15,14 +15,12 @@ const filesUsingHotReload = new Set();
15
15
  let assetsDirectory = "";
16
16
 
17
17
  /**
18
- * @param {"build" | "serve"} command
18
+ * @param {"build" | "serve" | undefined} _command
19
19
  * @param {import('../types/needleConfig').needleMeta | null} config
20
20
  * @param {import('../types').userSettings} userSettings
21
21
  * @returns {import('vite').Plugin | undefined}
22
22
  */
23
- export function needleReload(command, config, userSettings) {
24
- if (command === "build") return undefined;
25
-
23
+ export function needleReload(_command, config, userSettings) {
26
24
  if (userSettings?.noReload === true) return undefined;
27
25
 
28
26
 
@@ -51,6 +49,7 @@ export function needleReload(command, config, userSettings) {
51
49
 
52
50
  return {
53
51
  name: 'needle:reload',
52
+ apply: 'serve',
54
53
  /** @param {import('vite').UserConfig} config */
55
54
  config(config) {
56
55
  if (!config.server) config.server = { watch: { ignored: [] } };
@@ -174,7 +173,7 @@ async function handleReload({ file, server, modules: _modules, read, buildDirect
174
173
  // meaning if its the first export after the server starts those will not trigger a reload
175
174
  // Ignore d.ts
176
175
  const shouldCheckIfContentChanged = file.includes("/codegen/") || file.includes("/generated/") || file.endsWith("gen.js");// || file.endsWith(".glb") || file.endsWith(".gltf") || file.endsWith(".bin");
177
- if (shouldCheckIfContentChanged && !file.endsWith(".d.ts") && !file.endsWith("needle-html-data.json")) {
176
+ if (!file.includes("/.svelte-kit/") && !file.endsWith(".d.ts") && !file.endsWith("needle-html-data.json") && shouldCheckIfContentChanged) {
178
177
  if (reloadIsScheduled) {
179
178
  return [];
180
179
  }
package/src/engine/api.ts CHANGED
@@ -450,4 +450,4 @@ export * from "./xr/api.js"
450
450
  * import type { SceneData } from "@needle-tools/engine";
451
451
  * type OrbitSettings = SceneData["Camera"]["OrbitControls"];
452
452
  */
453
- export type { SceneData } from "needle:bindings";
453
+ export type { SceneData } from "needle-bindings";
@@ -38,7 +38,7 @@ import { Physics } from './engine_physics.js';
38
38
  import { PlayerViewManager } from './engine_playerview.js';
39
39
  import { RendererData as SceneLighting } from './engine_scenelighting.js';
40
40
  import { getSceneData } from './engine_scenedata.js';
41
- import type { SceneData } from 'needle:bindings';
41
+ import type { SceneData } from 'needle-bindings';
42
42
  import { getTempColor, logHierarchy } from './engine_three_utils.js';
43
43
  import { Time } from './engine_time.js';
44
44
  import { patchTonemapping } from './engine_tonemapping.js';
@@ -542,8 +542,22 @@ export class Context implements IContext {
542
542
  connection: NetworkConnection;
543
543
  /** @deprecated AssetDatabase is deprecated */
544
544
  assets: AssetDatabase;
545
- /** The main light in the scene */
546
- mainLight: ILight | null = null;
545
+
546
+ /** All registered lights in the scene, maintained by the Light component */
547
+ readonly lights: Set<ILight> = new Set();
548
+
549
+ /** The brightest registered directional light, or null if none are registered
550
+ * @see lights
551
+ */
552
+ get mainLight(): ILight | null {
553
+ let best: ILight | null = null;
554
+ for (const light of this.lights) {
555
+ if (light.type !== "directional") continue;
556
+ if (!best || light.intensity > best.intensity) best = light;
557
+ }
558
+ return best;
559
+ }
560
+
547
561
  /** @deprecated Use sceneLighting */
548
562
  get rendererData() { return this.sceneLighting }
549
563
  /** Access the scene lighting manager to control lighting settings in the context */
@@ -858,6 +872,8 @@ export class Context implements IContext {
858
872
  this._onBeforeRenderListeners.clear();
859
873
  this._onAfterRenderListeners.clear();
860
874
 
875
+ this.lights.clear();
876
+
861
877
  if (!this.isManagedExternally) {
862
878
  if (this.renderer) {
863
879
  this.renderer.renderLists.dispose();
@@ -11,6 +11,8 @@ import { initCameraExtensions } from "./js-extensions/Camera.js";
11
11
  import { patchLayers } from "./js-extensions/Layers.js";
12
12
  import { initObject3DExtensions } from "./js-extensions/Object3D.js";
13
13
  import { initVectorExtensions } from "./js-extensions/Vector.js";
14
+ import { SSR } from "./engine_ssr.js";
15
+ import { initLicense } from "./engine_license.js";
14
16
  import { initWebComponents } from "./webcomponents/init.js";
15
17
  import { initPhysics } from "./engine_physics_rapier.js";
16
18
  import { initXR } from "./xr/init.js";
@@ -28,6 +30,7 @@ let initialized = false;
28
30
  */
29
31
  export function initEngine() {
30
32
  if (initialized) return;
33
+ if (SSR) return;
31
34
  initialized = true;
32
35
 
33
36
  initWebComponents();
@@ -46,4 +49,5 @@ export function initEngine() {
46
49
  initSceneSwitcherAttributes();
47
50
  initPhysics();
48
51
  initXR();
52
+ initLicense();
49
53
  }
@@ -1,6 +1,3 @@
1
- import { dof } from "three/src/nodes/TSL.js";
2
-
3
- import { isDevEnvironment } from "./debug/index.js";
4
1
  import { BUILD_TIME, GENERATOR, PUBLIC_KEY, VERSION } from "./engine_constants.js";
5
2
  import { ContextEvent, ContextRegistry } from "./engine_context_registry.js";
6
3
  import { onInitialized } from "./engine_lifecycle_api.js";
@@ -8,7 +5,7 @@ import { isLocalNetwork } from "./engine_networking_utils.js";
8
5
  import { Context } from "./engine_setup.js";
9
6
  import type { IContext } from "./engine_types.js";
10
7
  import { getParam } from "./engine_utils.js";
11
- import { InternalAttributeUtils } from "./engine_utils_attributes.js";
8
+ import { SSR } from "./engine_ssr.js";
12
9
 
13
10
  const debug = getParam("debuglicense");
14
11
 
@@ -90,7 +87,9 @@ export namespace Telemetry {
90
87
  });
91
88
  }
92
89
 
93
- onInitialized((ctx => sendPageViewEvent(ctx)), { once: true });
90
+ export function init() {
91
+ if(!SSR) onInitialized((ctx => sendPageViewEvent(ctx)), { once: true });
92
+ }
94
93
 
95
94
  function sendPageViewEvent(ctx: IContext): Promise<void> | void {
96
95
  if (!isAllowed(ctx)) {
@@ -243,11 +242,14 @@ export namespace Telemetry {
243
242
  }
244
243
 
245
244
 
246
- ContextRegistry.registerCallback(ContextEvent.ContextRegistered, evt => {
247
- showLicenseInfo(evt.context);
248
- handleForbidden(evt.context);
249
- setTimeout(() => sendUsageMessageToAnalyticsBackend(evt.context), 2000);
250
- });
245
+ export function initLicense() {
246
+ Telemetry.init();
247
+ ContextRegistry.registerCallback(ContextEvent.ContextRegistered, evt => {
248
+ showLicenseInfo(evt.context);
249
+ handleForbidden(evt.context);
250
+ setTimeout(() => sendUsageMessageToAnalyticsBackend(evt.context), 2000);
251
+ });
252
+ }
251
253
 
252
254
  export let runtimeLicenseCheckPromise: Promise<void> | undefined = undefined;
253
255
  let applicationIsForbidden = false;
@@ -1,5 +1,6 @@
1
1
  import { type Context, FrameEvent } from "./engine_context.js";
2
2
  import { ContextEvent } from "./engine_context_registry.js";
3
+ import { SSR } from "./engine_ssr.js";
3
4
 
4
5
  export declare type Event = ContextEvent | FrameEvent;
5
6
 
@@ -38,6 +39,12 @@ let methodsWarningCounter = 0;
38
39
  * @param evt the event to call the function at
39
40
  */
40
41
  export function registerFrameEventCallback(cb: LifecycleMethod, evt: Event, opts?: LifecycleMethodOptions) {
42
+
43
+ // Don't register the callback if we are in SSR, because it will never be called and might cause memory leaks
44
+ if(SSR) {
45
+ return;
46
+ }
47
+
41
48
  if (!newMethods.has(evt)) {
42
49
  newMethods.set(evt, new Array());
43
50
  }
@@ -1,8 +1,8 @@
1
1
  import { createLoaders } from "@needle-tools/gltf-progressive";
2
2
  import { CubeUVReflectionMapping, EquirectangularRefractionMapping, SRGBColorSpace, Texture, TextureLoader, WebGLRenderer } from "three";
3
- import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader";
4
- import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader";
5
- import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";
3
+ import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader.js";
4
+ import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader.js";
5
+ import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js";
6
6
 
7
7
  const running: Map<string, Promise<Texture | null>> = new Map();
8
8
 
@@ -1,4 +1,5 @@
1
- import type { SceneData } from "needle:bindings";
1
+ import type { SceneData } from "needle-bindings";
2
+ export type { SceneData };
2
3
  import type { IContext } from "./engine_types.js";
3
4
  import { getComponent } from "./engine_components.js";
4
5
  import { TypeStore } from "./engine_typestore.js";
@@ -70,36 +71,58 @@ function makeErrorProxy(message: string): object {
70
71
  return new Proxy({}, handler);
71
72
  }
72
73
 
74
+ /**
75
+ * Returns a proxy for a scene node that exposes `$object`, `$components`,
76
+ * and child nodes as nested properties.
77
+ */
78
+ function makeNodeProxy(ctx: IContext, node: import("three").Object3D): object {
79
+ return new Proxy({}, {
80
+ get(_t, prop: string) {
81
+ if (prop === "$object") return node;
82
+ if (prop === "$components") {
83
+ return new Proxy({}, {
84
+ get(_t2, compName: string) {
85
+ if (compName === "then") return undefined;
86
+ const ctor = TypeStore.get(compName);
87
+ if (!ctor) return makeErrorProxy(`Component type "${compName}" not registered (node "${node.name}")`);
88
+ const comp = getComponent(node, ctor);
89
+ if (!comp) return makeErrorProxy(`Component "${compName}" not found on node "${node.name}"`);
90
+ return comp;
91
+ }
92
+ });
93
+ }
94
+ // Child node lookup by name
95
+ const child = node.children.find(c => c.name === prop) ?? null;
96
+ if (!child) return makeErrorProxy(`Child "${prop}" not found on node "${node.name}"`);
97
+ return makeNodeProxy(ctx, child);
98
+ }
99
+ });
100
+ }
101
+
73
102
  /**
74
103
  * Returns a lazily-resolved proxy typed as {@link SceneData}.
75
104
  * The proxy is cached per context — each context gets exactly one instance.
76
105
  *
77
- * Accessing a property traverses the scene graph on demand:
78
- * - First level → find Object3D by node name
79
- * - Second level → getComponent by component name (via TypeStore)
80
- * - Third level → read/write a field on the real component instance
81
- *
82
- * If a node or component is not found, property accesses log a warning
83
- * instead of throwing, so chained access never crashes.
106
+ * Shape mirrors the generated `needle-bindings.gen.d.ts`:
107
+ * ctx.sceneData.MyGlb.Camera.$components.OrbitControls.autoRotate = true;
108
+ * ctx.sceneData.MyGlb.Camera.$object // → THREE.Camera
109
+ * ctx.sceneData.MyGlb.UI.Button.$components.Button // → Needle Button component
84
110
  *
85
- * @example
86
- * ctx.sceneData.Camera.OrbitControls.autoRotate = true;
111
+ * GLB name is ignored at runtime (scene is already loaded).
112
+ * Node lookup starts at the scene root.
87
113
  */
88
114
  export function getSceneData(ctx: IContext): SceneData {
89
115
  let proxy = cache.get(ctx);
90
116
  if (!proxy) {
91
117
  proxy = new Proxy({} as SceneData, {
92
- get(_target, nodeName: string) {
118
+ get(_target, _glbName: string) {
119
+ // GLB name level — ignored at runtime, return node-name proxy
93
120
  return new Proxy({}, {
94
- get(_target, compName: string) {
121
+ get(_target, nodeName: string) {
122
+ if (nodeName === "then") return undefined;
95
123
  const node = ctx.scene.getObjectByName(nodeName) ?? null;
96
124
  if (!node) return makeErrorProxy(`Node "${nodeName}" not found in scene`);
97
- if (compName === "$node") return node;
98
- const ctor = TypeStore.get(compName);
99
- if (!ctor) return makeErrorProxy(`Component type "${compName}" not registered (node "${nodeName}")`);
100
- const comp = getComponent(node, ctor);
101
- if (!comp) return makeErrorProxy(`Component "${compName}" not found on node "${nodeName}"`);
102
- return comp;
125
+ return makeNodeProxy(ctx, node);
103
126
  }
104
127
  });
105
128
  }