@needle-tools/engine 3.4.0-alpha → 3.5.1-alpha

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 (79) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/needle-engine.js +59388 -59097
  3. package/dist/needle-engine.min.js +416 -391
  4. package/dist/needle-engine.umd.cjs +388 -363
  5. package/lib/engine/api.d.ts +1 -0
  6. package/lib/engine/api.js +1 -0
  7. package/lib/engine/api.js.map +1 -1
  8. package/lib/engine/engine_context.d.ts +9 -4
  9. package/lib/engine/engine_context.js +57 -32
  10. package/lib/engine/engine_context.js.map +1 -1
  11. package/lib/engine/engine_context_registry.d.ts +5 -3
  12. package/lib/engine/engine_context_registry.js +10 -2
  13. package/lib/engine/engine_context_registry.js.map +1 -1
  14. package/lib/engine/engine_element.js.map +1 -1
  15. package/lib/engine/engine_element_loading.js +2 -3
  16. package/lib/engine/engine_element_loading.js.map +1 -1
  17. package/lib/engine/engine_input.d.ts +2 -2
  18. package/lib/engine/engine_physics.d.ts +20 -93
  19. package/lib/engine/engine_physics.js +20 -892
  20. package/lib/engine/engine_physics.js.map +1 -1
  21. package/lib/engine/engine_physics.types.js.map +1 -1
  22. package/lib/engine/engine_physics_rapier.d.ts +103 -0
  23. package/lib/engine/engine_physics_rapier.js +1003 -0
  24. package/lib/engine/engine_physics_rapier.js.map +1 -0
  25. package/lib/engine/engine_types.d.ts +50 -1
  26. package/lib/engine/engine_types.js +8 -0
  27. package/lib/engine/engine_types.js.map +1 -1
  28. package/lib/engine-components/Collider.js +6 -6
  29. package/lib/engine-components/Collider.js.map +1 -1
  30. package/lib/engine-components/Joints.js +2 -2
  31. package/lib/engine-components/Joints.js.map +1 -1
  32. package/lib/engine-components/ReflectionProbe.js +16 -7
  33. package/lib/engine-components/ReflectionProbe.js.map +1 -1
  34. package/lib/engine-components/Renderer.js +3 -4
  35. package/lib/engine-components/Renderer.js.map +1 -1
  36. package/lib/engine-components/RigidBody.d.ts +0 -1
  37. package/lib/engine-components/RigidBody.js +24 -30
  38. package/lib/engine-components/RigidBody.js.map +1 -1
  39. package/lib/engine-components/export/usdz/ThreeUSDZExporter.js +52 -26
  40. package/lib/engine-components/export/usdz/ThreeUSDZExporter.js.map +1 -1
  41. package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.d.ts +8 -2
  42. package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.js +44 -7
  43. package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.js.map +1 -1
  44. package/lib/engine-components/ui/Canvas.js +29 -16
  45. package/lib/engine-components/ui/Canvas.js.map +1 -1
  46. package/lib/engine-components/ui/Layout.js +10 -5
  47. package/lib/engine-components/ui/Layout.js.map +1 -1
  48. package/lib/engine-components/ui/RectTransform.js +8 -3
  49. package/lib/engine-components/ui/RectTransform.js.map +1 -1
  50. package/lib/tsconfig.tsbuildinfo +1 -1
  51. package/package.json +1 -1
  52. package/plugins/vite/config.js +2 -1
  53. package/plugins/vite/defines.js +30 -0
  54. package/plugins/vite/dependency-watcher.js +173 -0
  55. package/plugins/vite/editor-connection.js +37 -39
  56. package/plugins/vite/index.js +5 -1
  57. package/plugins/vite/reload.js +3 -1
  58. package/src/engine/api.ts +1 -0
  59. package/src/engine/codegen/register_types.js +2 -2
  60. package/src/engine/engine_context.ts +75 -42
  61. package/src/engine/engine_context_registry.ts +13 -6
  62. package/src/engine/engine_element.ts +2 -1
  63. package/src/engine/engine_element_loading.ts +2 -3
  64. package/src/engine/engine_input.ts +2 -2
  65. package/src/engine/engine_physics.ts +25 -1020
  66. package/src/engine/engine_physics.types.ts +1 -3
  67. package/src/engine/engine_physics_rapier.ts +1127 -0
  68. package/src/engine/engine_types.ts +66 -4
  69. package/src/engine-components/Collider.ts +6 -6
  70. package/src/engine-components/Joints.ts +2 -2
  71. package/src/engine-components/ReflectionProbe.ts +17 -7
  72. package/src/engine-components/Renderer.ts +5 -5
  73. package/src/engine-components/RendererLightmap.ts +1 -1
  74. package/src/engine-components/RigidBody.ts +24 -31
  75. package/src/engine-components/export/usdz/ThreeUSDZExporter.ts +58 -29
  76. package/src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +51 -9
  77. package/src/engine-components/ui/Canvas.ts +29 -16
  78. package/src/engine-components/ui/Layout.ts +10 -5
  79. package/src/engine-components/ui/RectTransform.ts +9 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/engine",
3
- "version": "3.4.0-alpha",
3
+ "version": "3.5.1-alpha",
4
4
  "description": "Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in",
5
5
  "main": "dist/needle-engine.umd.cjs",
6
6
  "type": "module",
@@ -53,4 +53,5 @@ export function tryLoadProjectConfig() {
53
53
  /** "assets" -> the directory name inside the output directory to put e.g. glb files into */
54
54
  export function builtAssetsDirectory(){
55
55
  return "assets";
56
- }
56
+ }
57
+
@@ -0,0 +1,30 @@
1
+ import { loadConfig } from "./config.js";
2
+
3
+ /** used to pass config variables into vite.config.define
4
+ * for example "useRapier"
5
+ */
6
+ export const needleDefines = (command, config, userSettings) => {
7
+
8
+ if (!userSettings) userSettings = {};
9
+
10
+ let useRapier = true;
11
+ if (config.useRapier === false || userSettings?.useRapier === false) useRapier = false;
12
+
13
+ return {
14
+ name: 'needle-defines',
15
+ enforce: 'pre',
16
+ config(config) {
17
+ if (useRapier && userSettings?.useRapier !== true) {
18
+ const meta = loadConfig();
19
+ if (meta?.useRapier === false) {
20
+ useRapier = false;
21
+ }
22
+ }
23
+ console.log("UseRapier?", useRapier);
24
+ if (!config.define) config.define = {};
25
+ if (config.define.NEEDLE_USE_RAPIER === undefined) {
26
+ config.define.NEEDLE_USE_RAPIER = useRapier;
27
+ }
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,173 @@
1
+ import { exec, execSync } from 'child_process';
2
+ import { existsSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const prefix = "[needle-dependency-watcher] ";
7
+ function log(...msg) {
8
+ console.log(prefix, ...msg)
9
+ }
10
+
11
+ export const needleDependencyWatcher = (command, config, userSettings) => {
12
+ if (command === "build") return;
13
+
14
+ if (userSettings?.noDependencyWatcher === true) return;
15
+
16
+ const dir = process.cwd();
17
+ const packageJsonPath = path.join(dir, "package.json");
18
+ const viteCacheDir = path.join(dir, "node_modules", ".vite");
19
+
20
+ return {
21
+ name: 'needle-dependency-watcher',
22
+ configureServer(server) {
23
+ watchPackageJson(server, dir, packageJsonPath, viteCacheDir);
24
+ manageClients(server);
25
+ }
26
+ }
27
+ }
28
+
29
+ const currentClients = new Set();
30
+
31
+ function manageClients(server) {
32
+ server.ws.on("connection", (socket) => {
33
+ currentClients.add(socket);
34
+ socket.on("close", () => {
35
+ currentClients.delete(socket);
36
+ });
37
+ });
38
+ }
39
+
40
+ function triggerReloadOnClients() {
41
+ log("Triggering reload on clients (todo)", currentClients.size)
42
+ // for (const client of currentClients) {
43
+ // client.send(JSON.stringify({ type: "full-reload" }));
44
+ // }
45
+ }
46
+
47
+
48
+ let packageJsonStat;
49
+ let lastEditTime;
50
+ let packageJsonSize;
51
+ let packageJson;
52
+ let requireInstall = false;
53
+
54
+ function watchPackageJson(server, projectDir, packageJsonPath, cachePath) {
55
+
56
+ if (!existsSync(packageJsonPath)) {
57
+ return;
58
+ }
59
+
60
+ log("Watching project", packageJsonPath)
61
+
62
+ lastRestartTime = 0;
63
+ packageJsonStat = statSync(packageJsonPath);
64
+ lastEditTime = packageJsonStat.mtime;
65
+ packageJsonSize = packageJsonStat.size;
66
+ packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
67
+
68
+ setTimeout(() => {
69
+ requireInstall = testIfInstallIsRequired(projectDir, packageJson);
70
+ }, 1000);
71
+
72
+ setInterval(() => {
73
+ packageJsonStat = statSync(packageJsonPath);
74
+ let modified = false;
75
+ if (packageJsonStat.mtime > lastEditTime) {
76
+ modified = true;
77
+ }
78
+ if (packageJsonStat.size !== packageJsonSize) {
79
+ modified = true;
80
+ }
81
+ if (modified || requireInstall) {
82
+ if (modified)
83
+ log("package.json has changed")
84
+
85
+ let requireReload = false;
86
+ if (!requireInstall) {
87
+ requireInstall = testIfInstallIsRequired(projectDir, packageJson);
88
+ }
89
+
90
+ // test if dependencies changed
91
+ let newPackageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
92
+ for (const key in newPackageJson.dependencies) {
93
+ if (packageJson.dependencies[key] !== newPackageJson.dependencies[key] && newPackageJson.dependencies[key] !== undefined) {
94
+ log("Dependency added", key)
95
+ requireReload = true;
96
+ }
97
+ }
98
+
99
+
100
+ packageJsonSize = packageJsonStat.size;
101
+ lastEditTime = packageJsonStat.mtime;
102
+
103
+ if (requireReload || requireInstall) {
104
+ restart(server, projectDir, cachePath);
105
+ }
106
+ }
107
+ }, 1000);
108
+ }
109
+
110
+ function testIfInstallIsRequired(projectDir, packageJson) {
111
+
112
+ if (packageJson.dependencies) {
113
+ for (const key in packageJson.dependencies) {
114
+ // make sure the dependency is installed
115
+ const depPath = path.join(projectDir, "node_modules", key);
116
+ if (!existsSync(depPath)) {
117
+ log("Dependency not installed", key)
118
+ return true;
119
+ }
120
+ }
121
+ }
122
+ return false;
123
+ }
124
+
125
+ let isRunningRestart = false;
126
+ let restartId = 0;
127
+ let lastRestartTime = 0;
128
+
129
+ async function restart(server, projectDir, cachePath) {
130
+
131
+ if (isRunningRestart) return;
132
+ isRunningRestart = true;
133
+
134
+ try {
135
+ const id = ++restartId;
136
+
137
+ if (requireInstall) {
138
+ requireInstall = false;
139
+ log("Installing dependencies...")
140
+ execSync("npm install", { cwd: projectDir, stdio: "inherit" });
141
+ requireInstall = false;
142
+ }
143
+
144
+ if (id !== restartId) return;
145
+ if (Date.now() - lastRestartTime < 1000) return;
146
+ log("Restarting server...")
147
+ lastRestartTime = Date.now();
148
+ requireInstall = false;
149
+ if (existsSync(cachePath))
150
+ rmSync(cachePath, { recursive: true, force: true });
151
+ triggerReloadOnClients();
152
+
153
+ // touch vite config to trigger reload
154
+ // const viteConfigPath = path.join(projectDir, "vite.config.js");
155
+ // if (existsSync(viteConfigPath)) {
156
+ // const content = readFileSync(viteConfigPath, "utf8");
157
+ // writeFileSync(viteConfigPath, content, "utf8");
158
+ // isRunningRestart = false;
159
+ // return;
160
+ // }
161
+
162
+ // check if server is running
163
+ if (server.httpServer.listening)
164
+ server.restart();
165
+ isRunningRestart = false;
166
+ console.log("-----------------------------------------------")
167
+ }
168
+ catch (err) {
169
+ log("Error restarting server", err);
170
+ isRunningRestart = false;
171
+ }
172
+
173
+ }
@@ -73,48 +73,46 @@ function createPlugin(isInstalled) {
73
73
  },
74
74
 
75
75
  configureServer(server) {
76
- server.ws.on('connection', (socket, _request) => {
77
-
78
- // console.log("Send editor sync status: " + isInstalled);
79
- const reply = {
80
- type: "needle:editor-sync:installation-status",
81
- data: isInstalled
82
- }
83
- socket.send(JSON.stringify(reply));
84
-
85
- socket.on('message', async (bytes) => {
86
- if (bytes?.length < 50) {
87
- const message = Buffer.from(bytes).toString();
88
- if (message === "needle:editor:restart") {
89
- console.log("Received request for a soft restart of the vite server... restarting in 1 second")
90
- setTimeout(() => {
76
+ try
77
+ {
78
+ server.ws.on('connection', (socket, _request) => {
79
+
80
+ // console.log("Send editor sync status: " + isInstalled);
81
+ const reply = {
82
+ type: "needle:editor-sync:installation-status",
83
+ data: isInstalled
84
+ }
85
+ socket.send(JSON.stringify(reply));
86
+
87
+ socket.on('message', async (bytes) => {
88
+ if (bytes?.length < 50) {
89
+ const message = Buffer.from(bytes).toString();
90
+ if (message === "needle:editor:restart") {
91
+ console.log("Received request for a soft restart of the vite server... ")
91
92
  // This just restarts the vite server
92
93
  server.restart();
93
- // TODO: restart isnt recommended right now because e.g. Unity doesnt properly find the new process to display it in the progress bar
94
- // spawn(process.argv.shift(), process.argv, {
95
- // cwd: process.cwd(),
96
- // detached: true,
97
- // stdio: "inherit"
98
- // });
99
- // process.exit();
100
- }, 1000);
101
- }
102
- else if (message === "needle:editor:stop") {
103
- process.exit();
104
- }
105
- else if (message === `{"type":"ping"}`) {
106
- socket.send(JSON.stringify({ type: "pong" }));
94
+ }
95
+ else if (message === "needle:editor:stop") {
96
+ process.exit();
97
+ }
98
+ else if (message === `{"type":"ping"}`) {
99
+ socket.send(JSON.stringify({ type: "pong" }));
100
+ }
101
+ else if (message === "needle:editor:editor-sync-enabled") {
102
+ console.log("Editor sync enabled")
103
+ editorSyncEnabled = true;
104
+ }
105
+ else if (message === "needle:editor:editor-sync-disabled") {
106
+ editorSyncEnabled = false;
107
+ }
107
108
  }
108
- else if (message === "needle:editor:editor-sync-enabled") {
109
- console.log("Editor sync enabled")
110
- editorSyncEnabled = true;
111
- }
112
- else if (message === "needle:editor:editor-sync-disabled") {
113
- editorSyncEnabled = false;
114
- }
115
- }
116
- })
117
- });
109
+ })
110
+ });
111
+ }
112
+ catch(err){
113
+ console.error("Error in needle-editor-connection")
114
+ console.error(err)
115
+ }
118
116
  }
119
117
 
120
118
  }
@@ -1,3 +1,4 @@
1
+ import { needleDefines } from "./defines.js";
1
2
  import { needleBuild } from "./build.js";
2
3
  import { needleMeta } from "./meta.js"
3
4
  import { needlePoster } from "./poster.js"
@@ -9,6 +10,7 @@ import { needleViteAlias } from "./alias.js";
9
10
  import { needleTransformCodegen } from "./transform-codegen.js";
10
11
  import { needleLicense } from "./license.js";
11
12
  import { needlePeerjs } from "./peer.js";
13
+ import { needleDependencyWatcher } from "./dependency-watcher.js";
12
14
 
13
15
  export * from "./gzip.js";
14
16
  export * from "./config.js";
@@ -22,6 +24,7 @@ export const needlePlugins = async (command, config, userSettings) => {
22
24
  // ensure we have user settings initialized with defaults
23
25
  userSettings = { ...defaultUserSettings, ...userSettings }
24
26
  const array = [
27
+ needleDefines(command, config, userSettings),
25
28
  needleLicense(command, config, userSettings),
26
29
  needleViteAlias(command, config, userSettings),
27
30
  needleMeta(command, config, userSettings),
@@ -31,7 +34,8 @@ export const needlePlugins = async (command, config, userSettings) => {
31
34
  needleCopyFiles(command, config, userSettings),
32
35
  needleTransformCodegen(command, config, userSettings),
33
36
  needleDrop(command, config, userSettings),
34
- needlePeerjs(command, config, userSettings)
37
+ needlePeerjs(command, config, userSettings),
38
+ needleDependencyWatcher(command, config, userSettings)
35
39
  ];
36
40
  array.push(await editorConnection(command, config, userSettings, array));
37
41
  return array;
@@ -14,6 +14,8 @@ let assetsDirectory = "";
14
14
  export const needleReload = (command, config, userSettings) => {
15
15
  if (command === "build") return;
16
16
 
17
+ if (userSettings?.noReload === true) return;
18
+
17
19
 
18
20
  let isUpdatingConfig = false;
19
21
  const updateConfig = async () => {
@@ -45,7 +47,7 @@ export const needleReload = (command, config, userSettings) => {
45
47
  else if (!config.server.watch.ignored) config.server.watch.ignored = [];
46
48
  for (const pattern of ignorePatterns)
47
49
  config.server.watch.ignored.push(pattern);
48
- if(config?.debug === true || userSettings?.debug === true)
50
+ if (config?.debug === true || userSettings?.debug === true)
49
51
  setTimeout(() => console.log("Updated server ignore patterns: ", config.server.watch.ignored), 100);
50
52
  },
51
53
  handleHotUpdate(args) {
package/src/engine/api.ts CHANGED
@@ -27,6 +27,7 @@ export * from "./engine_patcher"
27
27
  export * from "./engine_playerview"
28
28
  export * from "./engine_physics"
29
29
  export * from "./engine_physics.types"
30
+ export * from "./engine_physics_rapier"
30
31
  export * from "./engine_scenelighting"
31
32
  export * from "./engine_input";
32
33
  export * from "./engine_math";
@@ -1,5 +1,5 @@
1
1
  import { TypeStore } from "./../engine_typestore"
2
-
2
+
3
3
  // Import types
4
4
  import { __Ignore } from "../../engine-components/codegen/components";
5
5
  import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder";
@@ -214,7 +214,7 @@ import { XRGrabModel } from "../../engine-components/webxr/WebXRGrabRendering";
214
214
  import { XRGrabRendering } from "../../engine-components/webxr/WebXRGrabRendering";
215
215
  import { XRRig } from "../../engine-components/webxr/WebXRRig";
216
216
  import { XRState } from "../../engine-components/XRFlag";
217
-
217
+
218
218
  // Register types
219
219
  TypeStore.add("__Ignore", __Ignore);
220
220
  TypeStore.add("ActionBuilder", ActionBuilder);
@@ -1,7 +1,8 @@
1
- import { BufferGeometry, Camera, Clock, Color, DepthTexture, Group,
2
- Material, NearestFilter, NoToneMapping, Object3D, PCFSoftShadowMap,
3
- PerspectiveCamera, RGBAFormat, Scene, sRGBEncoding,
4
- Texture, WebGLRenderer, WebGLRenderTarget
1
+ import {
2
+ BufferGeometry, Camera, Clock, Color, DepthTexture, Group,
3
+ Material, NearestFilter, NoToneMapping, Object3D, PCFSoftShadowMap,
4
+ PerspectiveCamera, RGBAFormat, Scene, sRGBEncoding,
5
+ Texture, WebGLRenderer, WebGLRenderTarget
5
6
  } from 'three'
6
7
  import { Input } from './engine_input';
7
8
  import { Physics } from './engine_physics';
@@ -27,6 +28,7 @@ import { PlayerViewManager } from './engine_playerview';
27
28
  import { CoroutineData, ICamera, IComponent, IContext, ILight } from "./engine_types"
28
29
  import { destroy, foreachComponent } from './engine_gameobject';
29
30
  import { ContextEvent, ContextRegistry } from './engine_context_registry';
31
+ import { delay } from './engine_utils';
30
32
  // import { createCameraWithOrbitControl } from '../engine-components/CameraUtils';
31
33
 
32
34
 
@@ -78,7 +80,7 @@ export enum XRSessionMode {
78
80
  ImmersiveAR = "immersive-ar",
79
81
  }
80
82
 
81
- export declare type OnBeforeRenderCallback = (renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, material: Material, group: Group) => void
83
+ export declare type OnRenderCallback = (renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, material: Material, group: Group) => void
82
84
 
83
85
 
84
86
  export function registerComponent(script: IComponent, context?: Context) {
@@ -251,8 +253,8 @@ export class Context implements IContext {
251
253
  this.isManagedExternally = true;
252
254
  }
253
255
  else {
254
- this.renderer = new WebGLRenderer({
255
- antialias: true
256
+ this.renderer = new WebGLRenderer({
257
+ antialias: true
256
258
  });
257
259
 
258
260
  // some tonemapping other than "NONE" is required for adjusting exposure with EXR environments
@@ -346,12 +348,13 @@ export class Context implements IContext {
346
348
  camera.updateProjectionMatrix();
347
349
  }
348
350
 
349
- onCreate(buildScene?: (context: Context, loadingOptions?: LoadingOptions) => Promise<void>, opts?: LoadingOptions) {
351
+ async onCreate(buildScene?: (context: Context, loadingOptions?: LoadingOptions) => Promise<void>, opts?: LoadingOptions) {
350
352
  if (this._isCreated) {
351
353
  console.warn("Context already created");
352
354
  return null;
353
355
  }
354
356
  this._isCreated = true;
357
+ await delay(1);
355
358
  return this.internalOnCreate(buildScene, opts);
356
359
  }
357
360
 
@@ -437,33 +440,57 @@ export class Context implements IContext {
437
440
  }
438
441
  }
439
442
 
440
- private _onBeforeRenderListeners: { [key: string]: OnBeforeRenderCallback[] } = {};
443
+
444
+
445
+ private _onBeforeRenderListeners = new Map<string, OnRenderCallback[]>();
446
+ private _onAfterRenderListeners = new Map<string, OnRenderCallback[]>();
441
447
 
442
448
  /** use this to subscribe to onBeforeRender events on threejs objects */
443
- addBeforeRenderListener(target: Object3D, callback: OnBeforeRenderCallback) {
444
- if (!this._onBeforeRenderListeners[target.uuid]) {
445
- this._onBeforeRenderListeners[target.uuid] = [];
446
- const onBeforeRenderCallback = (renderer, scene, camera, geometry, material, group) => {
447
- const arr = this._onBeforeRenderListeners[target.uuid];
448
- if (!arr) return;
449
- for (let i = 0; i < arr.length; i++) {
450
- const fn = arr[i];
451
- fn(renderer, scene, camera, geometry, material, group);
452
- }
453
- }
454
- target.onBeforeRender = onBeforeRenderCallback as any;
449
+ addBeforeRenderListener(target: Object3D, callback: OnRenderCallback) {
450
+ if (!this._onBeforeRenderListeners.has(target.uuid)) {
451
+ this._onBeforeRenderListeners.set(target.uuid, []);
452
+ target.onBeforeRender = this._createRenderCallbackWrapper(target, this._onBeforeRenderListeners);
453
+ }
454
+ this._onBeforeRenderListeners.get(target.uuid)?.push(callback);
455
+ }
456
+ removeBeforeRenderListener(target: Object3D, callback: OnRenderCallback) {
457
+ if (this._onBeforeRenderListeners.has(target.uuid)) {
458
+ const arr = this._onBeforeRenderListeners.get(target.uuid)!;
459
+ const idx = arr.indexOf(callback);
460
+ if (idx >= 0) arr.splice(idx, 1);
455
461
  }
456
- this._onBeforeRenderListeners[target.uuid].push(callback);
457
462
  }
458
463
 
459
- removeBeforeRenderListener(target: Object3D, callback: OnBeforeRenderCallback) {
460
- if (this._onBeforeRenderListeners[target.uuid]) {
461
- const arr = this._onBeforeRenderListeners[target.uuid];
464
+ /** use this to subscribe to onAfterRender events on threejs objects */
465
+ addAfterRenderListener(target: Object3D, callback: OnRenderCallback) {
466
+ if (!this._onAfterRenderListeners.has(target.uuid)) {
467
+ this._onAfterRenderListeners.set(target.uuid, []);
468
+ target.onAfterRender = this._createRenderCallbackWrapper(target, this._onAfterRenderListeners);
469
+ }
470
+ this._onAfterRenderListeners.get(target.uuid)?.push(callback);
471
+ }
472
+ removeAfterRenderListener(target: Object3D, callback: OnRenderCallback) {
473
+ if (this._onAfterRenderListeners.has(target.uuid)) {
474
+ const arr = this._onAfterRenderListeners.get(target.uuid)!;
462
475
  const idx = arr.indexOf(callback);
463
476
  if (idx >= 0) arr.splice(idx, 1);
464
477
  }
465
478
  }
466
479
 
480
+
481
+ private _createRenderCallbackWrapper(target: Object3D, array: Map<string, OnRenderCallback[]>): OnRenderCallback {
482
+ return (renderer, scene, camera, geometry, material, group) => {
483
+ const arr = array.get(target.uuid);
484
+ if (!arr) return;
485
+ for (let i = 0; i < arr.length; i++) {
486
+ const fn = arr[i];
487
+ fn(renderer, scene, camera, geometry, material, group);
488
+ }
489
+ }
490
+ }
491
+
492
+
493
+
467
494
  private _requireDepthTexture: boolean = false;
468
495
  private _requireColorTexture: boolean = false;
469
496
  private _renderTarget?: WebGLRenderTarget;
@@ -498,8 +525,8 @@ export class Context implements IContext {
498
525
 
499
526
  private async internalOnCreate(buildScene?: (context: Context, opts?: LoadingOptions) => Promise<void>, opts?: LoadingOptions) {
500
527
 
501
- // TODO: we could configure if we need physics
502
- await this.physics.createWorld();
528
+ Context.Current = this;
529
+ await ContextRegistry.dispatchCallback(ContextEvent.ContextCreationStart, this);
503
530
 
504
531
  // load and create scene
505
532
  let prepare_succeeded = true;
@@ -512,7 +539,7 @@ export class Context implements IContext {
512
539
  console.error(err);
513
540
  prepare_succeeded = false;
514
541
  }
515
- if (!prepare_succeeded) return;
542
+ if (!prepare_succeeded) return false;
516
543
 
517
544
  this.internalOnUpdateVisible();
518
545
 
@@ -523,6 +550,9 @@ export class Context implements IContext {
523
550
 
524
551
  Context.Current = this;
525
552
 
553
+ // TODO: we could configure if we need physics
554
+ // await this.physics.engine?.initialize();
555
+
526
556
  // Setup
527
557
  Context.Current = this;
528
558
  for (let i = 0; i < this.new_scripts.length; i++) {
@@ -578,7 +608,7 @@ export class Context implements IContext {
578
608
  else {
579
609
  ContextRegistry.dispatchCallback(ContextEvent.MissingCamera, this);
580
610
  if (!this.mainCamera && !this.isManagedExternally)
581
- console.error("MISSING camera", this);
611
+ console.warn("Missing camera in main scene", this);
582
612
  }
583
613
  }
584
614
 
@@ -609,13 +639,13 @@ export class Context implements IContext {
609
639
  if (debug)
610
640
  logHierarchy(this.scene, true);
611
641
 
612
- ContextRegistry.dispatchCallback(ContextEvent.ContextCreated, this);
642
+ return ContextRegistry.dispatchCallback(ContextEvent.ContextCreated, this);
613
643
  }
614
644
 
615
645
  private _accumulatedTime = 0;
616
646
  private _framerateClock = new Clock();
617
647
 
618
- private render(_, frame : XRFrame) {
648
+ private render(_, frame: XRFrame) {
619
649
  this._xrFrame = frame;
620
650
 
621
651
  this._currentFrameEvent = FrameEvent.Undefined;
@@ -627,7 +657,7 @@ export class Context implements IContext {
627
657
  }
628
658
  this._accumulatedTime = 0;
629
659
  }
630
-
660
+
631
661
  this._stats?.begin();
632
662
 
633
663
  Context.Current = this;
@@ -696,16 +726,19 @@ export class Context implements IContext {
696
726
  this.executeCoroutines(FrameEvent.LateUpdate);
697
727
  if (this.onHandlePaused()) return;
698
728
 
699
- const physicsSteps = 1;
700
- const dt = this.time.deltaTime / physicsSteps;
701
- for (let i = 0; i < physicsSteps; i++) {
702
- this._currentFrameEvent = FrameEvent.PrePhysicsStep;
703
- this.executeCoroutines(FrameEvent.PrePhysicsStep);
704
- this.physics.step(dt);
705
- this._currentFrameEvent = FrameEvent.PostPhysicsStep;
706
- this.executeCoroutines(FrameEvent.PostPhysicsStep);
729
+ if (this.physics.engine) {
730
+ const physicsSteps = 1;
731
+ const dt = this.time.deltaTime / physicsSteps;
732
+ for (let i = 0; i < physicsSteps; i++) {
733
+ this._currentFrameEvent = FrameEvent.PrePhysicsStep;
734
+ this.executeCoroutines(FrameEvent.PrePhysicsStep);
735
+ this.physics.engine.step(dt);
736
+ this._currentFrameEvent = FrameEvent.PostPhysicsStep;
737
+ this.executeCoroutines(FrameEvent.PostPhysicsStep);
738
+ }
739
+ this.physics.engine.postStep();
707
740
  }
708
- this.physics.postStep();
741
+
709
742
  if (this.onHandlePaused()) return;
710
743
 
711
744
  if (this.isVisibleToUser) {
@@ -781,7 +814,7 @@ export class Context implements IContext {
781
814
  this.renderRequiredTextures();
782
815
  // if (camera === this.mainCameraComponent?.cam) {
783
816
  // if (this.mainCameraComponent.activeTexture) {
784
-
817
+
785
818
  // }
786
819
  // }
787
820
  if (this.composer && !this.isInXR) {
@@ -1,8 +1,10 @@
1
- import { IContext } from "./engine_types";
1
+ import { IComponent, IContext } from "./engine_types";
2
2
 
3
3
  export enum ContextEvent {
4
4
  /** called when the context is registered to the registry, the context is not fully initialized at this point */
5
5
  ContextRegistered = "ContextRegistered",
6
+ /** called before the first glb is loaded, can be used to initialize physics engine, is awaited */
7
+ ContextCreationStart = "ContextCreationStart",
6
8
  ContextCreated = "ContextCreated",
7
9
  ContextDestroyed = "ContextDestroyed",
8
10
  MissingCamera = "MissingCamera",
@@ -13,11 +15,11 @@ export type ContextEventArgs = {
13
15
  context: IContext;
14
16
  }
15
17
 
16
- export type ContextCallback = (evt: ContextEventArgs) => void;
18
+ export type ContextCallback = (evt: ContextEventArgs) => void | Promise<any> | IComponent;
17
19
 
18
20
  export class ContextRegistry {
19
21
 
20
- static get Current(): IContext{
22
+ static get Current(): IContext {
21
23
  return globalThis["NeedleEngine.Context.Current"]
22
24
  }
23
25
  static set Current(ctx: IContext) {
@@ -51,10 +53,15 @@ export class ContextRegistry {
51
53
  this._callbacks[evt].splice(index, 1);
52
54
  }
53
55
 
54
- static dispatchCallback(evt: ContextEvent, context:IContext) {
55
- if (!this._callbacks[evt]) return;
56
+ static dispatchCallback(evt: ContextEvent, context: IContext) {
57
+ if (!this._callbacks[evt]) return true;
56
58
  const args = { event: evt, context }
57
- this._callbacks[evt].forEach(cb => cb(args));
59
+ const promises = new Array<Promise<any>>();
60
+ this._callbacks[evt].forEach(cb => {
61
+ const res = cb(args)
62
+ if (res instanceof Promise) promises.push(res);
63
+ });
64
+ return Promise.all(promises);
58
65
  }
59
66
 
60
67
  static addContextCreatedCallback(callback: ContextCallback) {