Needle Engine

Changes between version 4.3.0 and 4.2.6
Files changed (44) hide show
  1. plugins/vite/alias.js +3 -6
  2. src/engine-components/Animator.ts +22 -142
  3. src/engine-components/AnimatorController.ts +34 -184
  4. src/engine-components/AudioListener.ts +5 -16
  5. src/engine-components/AudioSource.ts +39 -137
  6. src/engine-components/AvatarLoader.ts +2 -61
  7. src/engine-components/AxesHelper.ts +1 -21
  8. src/engine-components/BoxHelperComponent.ts +0 -26
  9. src/engine-components/Camera.ts +41 -147
  10. src/engine-components/CameraUtils.ts +0 -20
  11. src/engine-components/Collider.ts +27 -102
  12. src/engine-components/Component.ts +129 -605
  13. src/engine-components/DragControls.ts +38 -134
  14. src/engine-components/DropListener.ts +23 -143
  15. src/engine/engine_addressables.ts +2 -33
  16. src/engine/engine_context.ts +1 -1
  17. src/engine/engine_input.ts +1 -20
  18. src/engine/engine_mainloop_utils.ts +4 -2
  19. src/engine/engine_math.ts +7 -25
  20. src/engine/engine_serialization_core.ts +1 -1
  21. src/engine/engine_three_utils.ts +14 -22
  22. src/engine/engine_types.ts +18 -179
  23. src/engine/engine_utils_screenshot.ts +3 -17
  24. src/engine-components/ui/EventSystem.ts +7 -9
  25. src/engine-components/Light.ts +44 -105
  26. src/engine-components/NeedleMenu.ts +11 -29
  27. src/engine/xr/NeedleXRSession.ts +1 -7
  28. src/engine-components/Networking.ts +6 -37
  29. src/engine-components/particlesystem/ParticleSystem.ts +2 -2
  30. src/engine-components-experimental/networking/PlayerSync.ts +13 -85
  31. src/engine-components/postprocessing/PostProcessingEffect.ts +1 -7
  32. src/engine-components/postprocessing/PostProcessingHandler.ts +6 -6
  33. src/engine-components/RigidBody.ts +4 -6
  34. src/engine-components/SceneSwitcher.ts +9 -78
  35. src/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusionN8.ts +8 -55
  36. src/engine-components/SpatialTrigger.ts +3 -80
  37. src/engine-components/SpectatorCamera.ts +18 -136
  38. src/engine-components/SpriteRenderer.ts +6 -22
  39. src/engine-components/SyncedTransform.ts +7 -50
  40. src/engine-components/TransformGizmo.ts +4 -49
  41. src/engine-components/postprocessing/Volume.ts +7 -5
  42. src/engine-components/postprocessing/VolumeProfile.ts +2 -10
  43. src/engine-components/webxr/WebARSessionRoot.ts +8 -31
  44. src/engine-components/webxr/WebXR.ts +29 -173
plugins/vite/alias.js CHANGED
@@ -57,8 +57,6 @@
57
57
  }
58
58
  if (debug) {
59
59
  const outputFilePath = path.resolve(projectDir, 'node_modules/.vite/needle.alias.log');
60
- const outputDirectory = path.dirname(outputFilePath);
61
- if (!existsSync(outputDirectory)) mkdirSync(outputDirectory, { recursive: true });
62
60
  outputDebugFile = createWriteStream(outputFilePath, { flags: "a" });
63
61
  const timestamp = new Date().toISOString();
64
62
  outputDebugFile.write("\n\n\n--------------------------\n");
@@ -70,7 +68,7 @@
70
68
  const aliasPlugin = {
71
69
  name: "needle-alias",
72
70
  config(config) {
73
- if (debug) console.log('[needle-alias] ProjectDirectory: ' + projectDir);
71
+ if (debug) console.log('[needle-alias] ProjectDirectory: ' + projectDir);
74
72
  if (!config.resolve) config.resolve = {};
75
73
  if (!config.resolve.alias) config.resolve.alias = {};
76
74
  const aliasDict = config.resolve.alias;
@@ -99,7 +97,6 @@
99
97
  let lastImporter = "";
100
98
  /** This plugin logs all imports. This helps to find cases where incorrect folders are found/resolved. */
101
99
 
102
- /** @type {import("vite").Plugin} */
103
100
  const debuggingPlugin = {
104
101
  name: "needle:alias-debug",
105
102
  // needs to run before regular resolver
@@ -118,9 +115,9 @@
118
115
  // verbose logging for all imports
119
116
  if (lastImporter !== importer) {
120
117
  lastImporter = importer;
121
- log(`[needle-alias] Resolving: ${importer} (file${options?.ssr ? ", SSR" : ""})`);
118
+ log('[needle-alias] Resolving: ', importer, "(file)");
122
119
  }
123
- log(`[needle-alias] ${id}`);
120
+ log('[needle-alias] ' + id);
124
121
  return;
125
122
  },
126
123
  }
src/engine-components/Animator.ts CHANGED
@@ -11,73 +11,38 @@
11
11
 
12
12
  const debug = getParam("debuganimator");
13
13
 
14
- /**
15
- * Represents an event emitted by an animation mixer
16
- * @category Animation and Sequencing
17
- */
14
+
18
15
  export declare class MixerEvent {
19
- /** The type of event that occurred */
20
16
  type: string;
21
- /** The animation action that triggered this event */
22
17
  action: AnimationAction;
23
- /** Number of loops completed in this cycle */
24
18
  loopDelta: number;
25
- /** The animation mixer that emitted this event */
26
19
  target: AnimationMixer;
27
20
  }
28
21
 
29
- /**
30
- * Configuration options for playing animations
31
- * @category Animation and Sequencing
32
- */
33
22
  export declare class PlayOptions {
34
- /** Whether the animation should loop, and if so, which loop style to use */
35
23
  loop?: boolean | AnimationActionLoopStyles;
36
- /** Whether the final animation state should be maintained after playback completes */
37
24
  clampWhenFinished?: boolean;
38
25
  }
39
26
 
40
- /**
41
- * The Animator component plays and manages animations on a GameObject.
42
- * It works with an AnimatorController to handle state transitions and animation blending.
43
- * A new AnimatorController can be created from code via `AnimatorController.createFromClips`.
27
+ /** The Animator component is used to play animations on a GameObject. It is used in combination with an AnimatorController (which is a state machine for animations)
28
+ * A new AnimatorController can be created from code via `AnimatorController.createFromClips`
44
29
  * @category Animation and Sequencing
45
30
  * @group Components
46
31
  */
47
32
  export class Animator extends Behaviour implements IAnimationComponent {
48
33
 
49
- /**
50
- * Identifies this component as an animation component in the engine
51
- */
52
34
  get isAnimationComponent() {
53
35
  return true;
54
36
  }
55
37
 
56
- /**
57
- * When enabled, animation will affect the root transform position and rotation
58
- */
59
38
  @serializable()
60
39
  applyRootMotion: boolean = false;
61
-
62
- /**
63
- * Indicates whether this animator contains root motion data
64
- */
65
40
  @serializable()
66
41
  hasRootMotion: boolean = false;
67
-
68
- /**
69
- * When enabled, the animator will maintain its state when the component is disabled
70
- */
71
42
  @serializable()
72
43
  keepAnimatorControllerStateOnDisable: boolean = false;
73
44
 
74
45
  // set from needle animator extension
75
- /**
76
- * Sets or replaces the animator controller for this component.
77
- * Handles binding the controller to this animator instance and ensures
78
- * proper initialization when the controller changes.
79
- * @param val The animator controller model or instance to use
80
- */
81
46
  @serializable()
82
47
  set runtimeAnimatorController(val: AnimatorControllerModel | AnimatorController | undefined | null) {
83
48
  if (this._animatorController && this._animatorController.model === val) {
@@ -104,53 +69,41 @@
104
69
  }
105
70
  else this._animatorController = null;
106
71
  }
107
-
108
- /**
109
- * Gets the current animator controller instance
110
- * @returns The current animator controller or null if none is assigned
111
- */
112
72
  get runtimeAnimatorController(): AnimatorController | undefined | null {
113
73
  return this._animatorController;
114
74
  }
115
75
 
116
- /**
117
- * Retrieves information about the current animation state
118
- * @returns The current state information, or undefined if no state is playing
119
- */
76
+ /** The current state info of the animator.
77
+ * If you just want to access the currently playing animation action you can use currentAction
78
+ * @returns {AnimatorStateInfo} The current state info of the animator or null if no state is playing
79
+ */
120
80
  getCurrentStateInfo() {
121
81
  return this.runtimeAnimatorController?.getCurrentStateInfo();
122
82
  }
123
- /**
124
- * The currently playing animation action that can be used to modify animation properties
125
- * @returns The current animation action, or null if no animation is playing
126
- */
83
+ /** The current action playing. It can be used to modify the action
84
+ * @returns {AnimationAction | null} The current action playing or null if no state is playing
85
+ */
127
86
  get currentAction() {
128
87
  return this.runtimeAnimatorController?.currentAction || null;
129
88
  }
130
89
 
131
- /**
132
- * Indicates whether animation parameters have been modified since the last update
133
- * @returns True if parameters have been changed
134
- */
90
+ /** @returns {boolean} True if parameters have been changed */
135
91
  get parametersAreDirty() { return this._parametersAreDirty; }
136
92
  private _parametersAreDirty: boolean = false;
137
93
 
138
- /**
139
- * Indicates whether the animator state has changed since the last update
140
- * @returns True if the animator has been changed
141
- */
94
+ /** @returns {boolean} True if the animator has been changed */
142
95
  get isDirty() { return this._isDirty; }
143
96
  private _isDirty: boolean = false;
144
97
 
145
98
  /**@deprecated use play() */
146
99
  Play(name: string | number, layer: number = -1, normalizedTime: number = Number.NEGATIVE_INFINITY, transitionDurationInSec: number = 0) { this.play(name, layer, normalizedTime, transitionDurationInSec); }
147
- /**
148
- * Plays an animation on the animator
149
- * @param name The name or hash of the animation to play
150
- * @param layer The layer to play the animation on (-1 for default layer)
151
- * @param normalizedTime The time position to start playing (0-1 range, NEGATIVE_INFINITY for current position)
152
- * @param transitionDurationInSec The duration of the blend transition in seconds
153
- */
100
+ /** Plays an animation on the animator
101
+ * @param {string | number} name The name of the animation to play. Can also be the hash of the animation
102
+ * @param {number} layer The layer to play the animation on. Default is -1
103
+ * @param {number} normalizedTime The normalized time to start the animation at. Default is Number.NEGATIVE_INFINITY
104
+ * @param {number} transitionDurationInSec The duration of the transition to the new animation. Default is 0
105
+ * @returns {void}
106
+ * */
154
107
  play(name: string | number, layer: number = -1, normalizedTime: number = Number.NEGATIVE_INFINITY, transitionDurationInSec: number = 0) {
155
108
  this.runtimeAnimatorController?.play(name, layer, normalizedTime, transitionDurationInSec);
156
109
  this._isDirty = true;
@@ -158,9 +111,7 @@
158
111
 
159
112
  /**@deprecated use reset */
160
113
  Reset() { this.reset(); }
161
- /**
162
- * Resets the animator controller to its initial state
163
- */
114
+ /** Resets the animatorcontroller */
164
115
  reset() {
165
116
  this._animatorController?.reset();
166
117
  this._isDirty = true;
@@ -168,12 +119,6 @@
168
119
 
169
120
  /**@deprecated use setBool */
170
121
  SetBool(name: string | number, val: boolean) { this.setBool(name, val); }
171
-
172
- /**
173
- * Sets a boolean parameter in the animator
174
- * @param name The name or hash of the parameter
175
- * @param value The boolean value to set
176
- */
177
122
  setBool(name: string | number, value: boolean) {
178
123
  if (debug) console.log("setBool", name, value);
179
124
  if (this.runtimeAnimatorController?.getBool(name) !== value)
@@ -183,34 +128,18 @@
183
128
 
184
129
  /**@deprecated use getBool */
185
130
  GetBool(name: string | number) { return this.getBool(name); }
186
-
187
- /**
188
- * Gets a boolean parameter from the animator
189
- * @param name The name or hash of the parameter
190
- * @returns The value of the boolean parameter, or false if not found
191
- */
192
131
  getBool(name: string | number): boolean {
193
132
  const res = this.runtimeAnimatorController?.getBool(name) ?? false;
194
133
  if (debug) console.log("getBool", name, res);
195
134
  return res;
196
135
  }
197
136
 
198
- /**
199
- * Toggles a boolean parameter between true and false
200
- * @param name The name or hash of the parameter
201
- */
202
137
  toggleBool(name: string | number) {
203
138
  this.setBool(name, !this.getBool(name));
204
139
  }
205
140
 
206
141
  /**@deprecated use setFloat */
207
142
  SetFloat(name: string | number, val: number) { this.setFloat(name, val); }
208
-
209
- /**
210
- * Sets a float parameter in the animator
211
- * @param name The name or hash of the parameter
212
- * @param val The float value to set
213
- */
214
143
  setFloat(name: string | number, val: number) {
215
144
  if (this.runtimeAnimatorController?.getFloat(name) !== val)
216
145
  this._parametersAreDirty = true;
@@ -220,12 +149,6 @@
220
149
 
221
150
  /**@deprecated use getFloat */
222
151
  GetFloat(name: string | number) { return this.getFloat(name); }
223
-
224
- /**
225
- * Gets a float parameter from the animator
226
- * @param name The name or hash of the parameter
227
- * @returns The value of the float parameter, or -1 if not found
228
- */
229
152
  getFloat(name: string | number): number {
230
153
  const res = this.runtimeAnimatorController?.getFloat(name) ?? -1;
231
154
  if (debug) console.log("getFloat", name, res);
@@ -234,12 +157,6 @@
234
157
 
235
158
  /**@deprecated use setInteger */
236
159
  SetInteger(name: string | number, val: number) { this.setInteger(name, val); }
237
-
238
- /**
239
- * Sets an integer parameter in the animator
240
- * @param name The name or hash of the parameter
241
- * @param val The integer value to set
242
- */
243
160
  setInteger(name: string | number, val: number) {
244
161
  if (this.runtimeAnimatorController?.getInteger(name) !== val)
245
162
  this._parametersAreDirty = true;
@@ -249,12 +166,6 @@
249
166
 
250
167
  /**@deprecated use getInteger */
251
168
  GetInteger(name: string | number) { return this.getInteger(name); }
252
-
253
- /**
254
- * Gets an integer parameter from the animator
255
- * @param name The name or hash of the parameter
256
- * @returns The value of the integer parameter, or -1 if not found
257
- */
258
169
  getInteger(name: string | number): number {
259
170
  const res = this.runtimeAnimatorController?.getInteger(name) ?? -1;
260
171
  if (debug) console.log("getInteger", name, res);
@@ -263,11 +174,6 @@
263
174
 
264
175
  /**@deprecated use setTrigger */
265
176
  SetTrigger(name: string | number) { this.setTrigger(name); }
266
-
267
- /**
268
- * Activates a trigger parameter in the animator
269
- * @param name The name or hash of the trigger parameter
270
- */
271
177
  setTrigger(name: string | number) {
272
178
  this._parametersAreDirty = true;
273
179
  if (debug) console.log("setTrigger", name);
@@ -276,11 +182,6 @@
276
182
 
277
183
  /**@deprecated use resetTrigger */
278
184
  ResetTrigger(name: string | number) { this.resetTrigger(name); }
279
-
280
- /**
281
- * Resets a trigger parameter in the animator
282
- * @param name The name or hash of the trigger parameter
283
- */
284
185
  resetTrigger(name: string | number) {
285
186
  this._parametersAreDirty = true;
286
187
  if (debug) console.log("resetTrigger", name);
@@ -289,12 +190,6 @@
289
190
 
290
191
  /**@deprecated use getTrigger */
291
192
  GetTrigger(name: string | number) { this.getTrigger(name); }
292
-
293
- /**
294
- * Gets the state of a trigger parameter from the animator
295
- * @param name The name or hash of the trigger parameter
296
- * @returns The state of the trigger parameter
297
- */
298
193
  getTrigger(name: string | number) {
299
194
  const res = this.runtimeAnimatorController?.getTrigger(name);
300
195
  if (debug) console.log("getTrigger", name, res);
@@ -303,21 +198,13 @@
303
198
 
304
199
  /**@deprecated use isInTransition */
305
200
  IsInTransition() { return this.isInTransition(); }
306
- /**
307
- * Checks if the animator is currently in a transition between states
308
- * @returns True if the animator is currently blending between animations
309
- */
201
+ /** @returns `true` if the animator is currently in a transition */
310
202
  isInTransition(): boolean {
311
203
  return this.runtimeAnimatorController?.isInTransition() ?? false;
312
204
  }
313
205
 
314
206
  /**@deprecated use setSpeed */
315
207
  SetSpeed(speed: number) { return this.setSpeed(speed); }
316
-
317
- /**
318
- * Sets the playback speed of the animator
319
- * @param speed The new playback speed multiplier
320
- */
321
208
  setSpeed(speed: number) {
322
209
  if (speed === this._speed) return;
323
210
  if (debug) console.log("setSpeed", speed);
@@ -326,20 +213,13 @@
326
213
  this._animatorController.setSpeed(speed);
327
214
  }
328
215
 
329
- /**
330
- * Sets a random playback speed between the min and max values
331
- * @param minMax Object with x (minimum) and y (maximum) speed values
332
- */
216
+ /** Will generate a random speed between the min and max values and set it to the animatorcontroller */
333
217
  set minMaxSpeed(minMax: { x: number, y: number }) {
334
218
  this._speed = Mathf.lerp(minMax.x, minMax.y, Math.random());
335
219
  if (this._animatorController?.animator == this)
336
220
  this._animatorController.setSpeed(this._speed);
337
221
  }
338
222
 
339
- /**
340
- * Sets a random normalized time offset for animations between min (x) and max (y) values
341
- * @param minMax Object with x (min) and y (max) values for the offset range
342
- */
343
223
  set minMaxOffsetNormalized(minMax: { x: number, y: number }) {
344
224
  this._normalizedStartOffset = Mathf.lerp(minMax.x, minMax.y, Math.random());
345
225
  if (this.runtimeAnimatorController?.animator == this)
src/engine-components/AnimatorController.ts CHANGED
@@ -15,11 +15,6 @@
15
15
  const debug = getParam("debuganimatorcontroller");
16
16
  const debugRootMotion = getParam("debugrootmotion");
17
17
 
18
- /**
19
- * Generates a hash code for a string
20
- * @param str - The string to hash
21
- * @returns A numeric hash value
22
- */
23
18
  function stringToHash(str): number {
24
19
  let hash = 0;
25
20
  for (let i = 0; i < str.length; i++) {
@@ -30,38 +25,27 @@
30
25
  return hash;
31
26
  }
32
27
 
33
- /**
34
- * Configuration options for creating an AnimatorController
35
- */
36
28
  declare type CreateAnimatorControllerOptions = {
37
- /** Should each animation state loop */
29
+ /** Should each animationstate loop */
38
30
  looping?: boolean,
39
- /** Set to false to disable generating transitions between animation clips */
31
+ /** Set to false to disable generating transitions between animationclips */
40
32
  autoTransition?: boolean,
41
- /** Duration in seconds for transitions between states */
33
+ /** Set to a positive value in seconds for transition duration between states */
42
34
  transitionDuration?: number,
43
35
  }
44
36
 
45
37
  /**
46
- * Controls the playback of animations using a state machine architecture.
47
- *
48
- * The AnimatorController manages animation states, transitions between states,
49
- * and parameters that affect those transitions. It is used by the {@link Animator}
50
- * component to control animation behavior on 3D models.
51
- *
52
- * Use the static method {@link AnimatorController.createFromClips} to create
53
- * an animator controller from a set of animation clips.
54
- */
38
+ * The AnimatorController is used to control the playback of animations. It is used by the {@link Animator} component.
39
+ * It is using a state machine to control the playback of animations.
40
+ * To create an animator controller use the static method `AnimatorController.createFromClips(clips: AnimationClip[], options: CreateAnimatorControllerOptions)`
41
+ */
55
42
  export class AnimatorController {
56
43
 
57
- /**
58
- * Creates an AnimatorController from a set of animation clips.
59
- * Each clip becomes a state in the controller's state machine.
60
- *
61
- * @param clips - The animation clips to use for creating states
62
- * @param options - Configuration options for the controller including looping behavior and transitions
63
- * @returns A new AnimatorController instance
64
- */
44
+ /** Create an animatorcontroller. States are created from the clips array.
45
+ * @param clips the clips to assign to the controller
46
+ * @param options options to control the creation of the controller.
47
+ * @returns the created animator controller
48
+ */
65
49
  static createFromClips(clips: AnimationClip[], options: CreateAnimatorControllerOptions = { looping: false, autoTransition: true, transitionDuration: 0 }): AnimatorController {
66
50
  const states: State[] = [];
67
51
  for (let i = 0; i < clips.length; i++) {
@@ -115,14 +99,6 @@
115
99
  return controller;
116
100
  }
117
101
 
118
- /**
119
- * Plays an animation state by name or hash.
120
- *
121
- * @param name - The name or hash identifier of the state to play
122
- * @param layerIndex - The layer index (defaults to 0)
123
- * @param normalizedTime - The normalized time to start the animation from (0-1)
124
- * @param durationInSec - Transition duration in seconds
125
- */
126
102
  play(name: string | number, layerIndex: number = -1, normalizedTime: number = Number.NEGATIVE_INFINITY, durationInSec: number = 0) {
127
103
  if (layerIndex < 0) layerIndex = 0;
128
104
  else if (layerIndex >= this.model.layers.length) {
@@ -142,42 +118,20 @@
142
118
  console.warn("Could not find " + name + " to play");
143
119
  }
144
120
 
145
- /**
146
- * Resets the controller to its initial state.
147
- */
148
121
  reset() {
149
122
  this.setStartTransition();
150
123
  }
151
124
 
152
- /**
153
- * Sets a boolean parameter value by name or hash.
154
- *
155
- * @param name - The name or hash identifier of the parameter
156
- * @param value - The boolean value to set
157
- */
158
125
  setBool(name: string | number, value: boolean) {
159
126
  const key = typeof name === "string" ? "name" : "hash";
160
127
  return this.model?.parameters?.filter(p => p[key] === name).forEach(p => p.value = value);
161
128
  }
162
129
 
163
- /**
164
- * Gets a boolean parameter value by name or hash.
165
- *
166
- * @param name - The name or hash identifier of the parameter
167
- * @returns The boolean value of the parameter, or false if not found
168
- */
169
130
  getBool(name: string | number): boolean {
170
131
  const key = typeof name === "string" ? "name" : "hash";
171
132
  return this.model?.parameters?.find(p => p[key] === name)?.value as boolean ?? false;
172
133
  }
173
134
 
174
- /**
175
- * Sets a float parameter value by name or hash.
176
- *
177
- * @param name - The name or hash identifier of the parameter
178
- * @param val - The float value to set
179
- * @returns True if the parameter was found and set, false otherwise
180
- */
181
135
  setFloat(name: string | number, val: number) {
182
136
  const key = typeof name === "string" ? "name" : "hash";
183
137
  const filtered = this.model?.parameters?.filter(p => p[key] === name);
@@ -185,45 +139,21 @@
185
139
  return filtered?.length > 0;
186
140
  }
187
141
 
188
- /**
189
- * Gets a float parameter value by name or hash.
190
- *
191
- * @param name - The name or hash identifier of the parameter
192
- * @returns The float value of the parameter, or 0 if not found
193
- */
194
142
  getFloat(name: string | number): number {
195
143
  const key = typeof name === "string" ? "name" : "hash";
196
144
  return this.model?.parameters?.find(p => p[key] === name)?.value as number ?? 0;
197
145
  }
198
146
 
199
- /**
200
- * Sets an integer parameter value by name or hash.
201
- *
202
- * @param name - The name or hash identifier of the parameter
203
- * @param val - The integer value to set
204
- */
205
147
  setInteger(name: string | number, val: number) {
206
148
  const key = typeof name === "string" ? "name" : "hash";
207
149
  return this.model?.parameters?.filter(p => p[key] === name).forEach(p => p.value = val);
208
150
  }
209
151
 
210
- /**
211
- * Gets an integer parameter value by name or hash.
212
- *
213
- * @param name - The name or hash identifier of the parameter
214
- * @returns The integer value of the parameter, or 0 if not found
215
- */
216
152
  getInteger(name: string | number): number {
217
153
  const key = typeof name === "string" ? "name" : "hash";
218
154
  return this.model?.parameters?.find(p => p[key] === name)?.value as number ?? 0;
219
155
  }
220
156
 
221
- /**
222
- * Sets a trigger parameter to active (true).
223
- * Trigger parameters are automatically reset after they are consumed by a transition.
224
- *
225
- * @param name - The name or hash identifier of the trigger parameter
226
- */
227
157
  setTrigger(name: string | number) {
228
158
  if (debug)
229
159
  console.log("SET TRIGGER", name);
@@ -231,32 +161,16 @@
231
161
  return this.model?.parameters?.filter(p => p[key] === name).forEach(p => p.value = true);
232
162
  }
233
163
 
234
- /**
235
- * Resets a trigger parameter to inactive (false).
236
- *
237
- * @param name - The name or hash identifier of the trigger parameter
238
- */
239
164
  resetTrigger(name: string | number) {
240
165
  const key = typeof name === "string" ? "name" : "hash";
241
166
  return this.model?.parameters?.filter(p => p[key] === name).forEach(p => p.value = false);
242
167
  }
243
168
 
244
- /**
245
- * Gets the current state of a trigger parameter.
246
- *
247
- * @param name - The name or hash identifier of the trigger parameter
248
- * @returns The boolean state of the trigger, or false if not found
249
- */
250
169
  getTrigger(name: string | number): boolean {
251
170
  const key = typeof name === "string" ? "name" : "hash";
252
171
  return this.model?.parameters?.find(p => p[key] === name)?.value as boolean ?? false;
253
172
  }
254
173
 
255
- /**
256
- * Checks if the controller is currently in a transition between states.
257
- *
258
- * @returns True if a transition is in progress, false otherwise
259
- */
260
174
  isInTransition(): boolean {
261
175
  return this._activeStates.length > 1;
262
176
  }
@@ -268,21 +182,8 @@
268
182
  private _speed: number = 1;
269
183
 
270
184
 
271
- /**
272
- * Finds an animation state by name or hash.
273
- * @deprecated Use findState instead
274
- *
275
- * @param name - The name or hash identifier of the state to find
276
- * @returns The found state or null if not found
277
- */
185
+ /**@deprecated use findState */
278
186
  FindState(name: string | number | undefined | null): State | null { return this.findState(name); }
279
-
280
- /**
281
- * Finds an animation state by name or hash.
282
- *
283
- * @param name - The name or hash identifier of the state to find
284
- * @returns The found state or null if not found
285
- */
286
187
  findState(name: string | number | undefined | null): State | null {
287
188
  if (!name) return null;
288
189
  if (Array.isArray(this.model.layers)) {
@@ -295,11 +196,9 @@
295
196
  return null;
296
197
  }
297
198
 
298
- /**
299
- * Gets information about the current playing animation state.
300
- *
301
- * @returns An AnimatorStateInfo object with data about the current state, or null if no state is active
302
- */
199
+ /** Get the current state info
200
+ * @returns the current state info or null if no state is active
201
+ */
303
202
  getCurrentStateInfo() {
304
203
  if (!this._activeState) return null;
305
204
  const action = this._activeState.motion.action;
@@ -309,11 +208,10 @@
309
208
  return new AnimatorStateInfo(this._activeState, normalizedTime, dur, this._speed);
310
209
  }
311
210
 
312
- /**
313
- * Gets the animation action currently playing.
314
- *
315
- * @returns The current animation action, or null if no action is playing
316
- */
211
+ /** Get the current action (shorthand for activeState.motion.action)
212
+ * @returns the current action that is playing. This is the action that is currently transitioning to or playing.
213
+ * If no action is playing null is returned.
214
+ **/
317
215
  get currentAction(): AnimationAction | null {
318
216
  if (!this._activeState) return null;
319
217
  const action = this._activeState.motion.action;
@@ -321,37 +219,24 @@
321
219
  return action;
322
220
  }
323
221
 
324
- /**
325
- * The normalized time (0-1) to start playing the first state at.
326
- * This affects the initial state when the animator is first enabled.
327
- */
222
+ /** The normalized time of the start state. This is used to determine the start time of the first state. */
328
223
  normalizedStartOffset: number = 0;
329
224
 
330
- /**
331
- * The Animator component this controller is bound to.
332
- */
225
+ /** the animator that this controller is bound to */
333
226
  animator?: Animator;
334
-
335
- /**
336
- * The data model describing the animation states and transitions.
337
- */
227
+ /** the model that this controller is based on */
338
228
  model: AnimatorControllerModel;
339
229
 
340
- /**
341
- * Gets the engine context from the bound animator.
342
- */
230
+ /** Get the context of the animator */
343
231
  get context(): Context | undefined | null { return this.animator?.context; }
344
232
 
345
- /**
346
- * Gets the animation mixer used by this controller.
347
- */
233
+ /** Get the animation mixer that is used to play the animations */
348
234
  get mixer() {
349
235
  return this._mixer;
350
236
  }
351
237
 
352
238
  /**
353
- * Cleans up resources used by this controller.
354
- * Stops all animations and unregisters the mixer from the animation system.
239
+ * Clears the animation mixer and unregisters it from the context.
355
240
  */
356
241
  dispose() {
357
242
  this._mixer.stopAllAction();
@@ -369,12 +254,7 @@
369
254
  // // this.internalApplyRootMotion(obj);
370
255
  // }
371
256
 
372
- /**
373
- * Binds this controller to an animator component.
374
- * Creates a new animation mixer and sets up animation actions.
375
- *
376
- * @param animator - The animator to bind this controller to
377
- */
257
+ /** Bind the animator to the controller. Only one animator can be bound to a controller at a time. */
378
258
  bind(animator: Animator) {
379
259
  if (!animator) console.error("AnimatorController.bind: animator is null");
380
260
  else if (this.animator !== animator) {
@@ -389,12 +269,7 @@
389
269
  }
390
270
  }
391
271
 
392
- /**
393
- * Creates a deep copy of this controller.
394
- * Clones the model data but does not copy runtime state.
395
- *
396
- * @returns A new AnimatorController instance with the same configuration
397
- */
272
+ /** Create a clone of the controller. This will clone the model but not the runtime state. */
398
273
  clone() {
399
274
  if (typeof this.model === "string") {
400
275
  console.warn("AnimatorController has not been resolved, can not create model from string", this.model);
@@ -422,12 +297,7 @@
422
297
  return controller;
423
298
  }
424
299
 
425
- /**
426
- * Updates the controller's state machine and animations.
427
- * Called each frame by the animator component.
428
- *
429
- * @param weight - The weight to apply to the animations (for blending)
430
- */
300
+ /** Called by the animator. This will update the active states and transitions as well as the animation mixer. */
431
301
  update(weight: number) {
432
302
  if (!this.animator) return;
433
303
  this.evaluateTransitions();
@@ -448,11 +318,9 @@
448
318
  private _mixer!: AnimationMixer;
449
319
  private _activeState?: State;
450
320
 
451
- /**
452
- * Gets the currently active animation state.
453
- *
454
- * @returns The active state or undefined if no state is active
455
- */
321
+ /** Get the currently active state playing
322
+ * @returns the currently active state or undefined if no state is active
323
+ **/
456
324
  get activeState(): State | undefined { return this._activeState; }
457
325
 
458
326
  constructor(model: AnimatorControllerModel) {
@@ -902,10 +770,6 @@
902
770
  }
903
771
  }
904
772
 
905
- /**
906
- * Yields all animation actions managed by this controller.
907
- * Iterates through all states in all layers and returns their actions.
908
- */
909
773
  *enumerateActions() {
910
774
  if (!this.model.layers) return;
911
775
  for (const layer of this.model.layers) {
@@ -943,10 +807,6 @@
943
807
  // }
944
808
  }
945
809
 
946
- /**
947
- * Wraps a KeyframeTrack to allow custom evaluation of animation values.
948
- * Used internally to modify animation behavior without changing the original data.
949
- */
950
810
  class TrackEvaluationWrapper {
951
811
 
952
812
  track?: KeyframeTrack;
@@ -984,10 +844,6 @@
984
844
  }
985
845
  }
986
846
 
987
- /**
988
- * Handles root motion extraction from animation tracks.
989
- * Captures movement from animations and applies it to the root object.
990
- */
991
847
  class RootMotionAction {
992
848
 
993
849
  private static lastObjPosition: { [key: string]: Vector3 } = {};
@@ -1187,10 +1043,6 @@
1187
1043
  }
1188
1044
  }
1189
1045
 
1190
- /**
1191
- * Manages root motion for a character.
1192
- * Extracts motion from animation tracks and applies it to the character's transform.
1193
- */
1194
1046
  class RootMotionHandler {
1195
1047
 
1196
1048
  private controller: AnimatorController;
@@ -1277,10 +1129,8 @@
1277
1129
  }
1278
1130
  }
1279
1131
 
1280
- /**
1281
- * Serialization handler for AnimatorController instances.
1282
- * Handles conversion between serialized data and runtime objects.
1283
- */
1132
+
1133
+
1284
1134
  class AnimatorControllerSerializator extends TypeSerializer {
1285
1135
  onSerialize(_: any, _context: SerializationContext) {
1286
1136
 
src/engine-components/AudioListener.ts CHANGED
@@ -5,18 +5,15 @@
5
5
  import { Behaviour, GameObject } from "./Component.js";
6
6
 
7
7
  /**
8
- * AudioListener represents a listener that can hear audio sources in the scene.
9
- * This component creates and manages a Three.js {@link three#AudioListener}, automatically connecting it
10
- * to the main camera or a Camera in the parent hierarchy.
8
+ * AudioListener represents a listener that can be attached to a GameObject to listen to audio sources in the scene.
11
9
  * @category Multimedia
12
10
  * @group Components
13
11
  */
14
12
  export class AudioListener extends Behaviour {
15
13
 
16
14
  /**
17
- * Gets the existing Three.js {@link three#AudioListener} instance or creates a new one if it doesn't exist.
18
- * This listener is responsible for capturing audio in the 3D scene.
19
- * @returns The {@link three#AudioListener} instance
15
+ * Gets the existing or creates a new {@link ThreeAudioListener} instance
16
+ * @returns The {@link ThreeAudioListener} instance
20
17
  */
21
18
  get listener(): ThreeAudioListener {
22
19
  if (this._listener == null)
@@ -26,21 +23,13 @@
26
23
 
27
24
  private _listener: ThreeAudioListener | null = null;
28
25
 
29
- /**
30
- * Registers for interaction events and initializes the audio listener
31
- * when this component is enabled.
32
- * @internal
33
- */
26
+ /** @internal */
34
27
  onEnable(): void {
35
28
  Application.registerWaitForInteraction(this.onInteraction);
36
29
  this.addListenerIfItExists();
37
30
  }
38
31
 
39
- /**
40
- * Cleans up event registrations and removes the audio listener
41
- * when this component is disabled.
42
- * @internal
43
- */
32
+ /** @internal */
44
33
  onDisable(): void {
45
34
  Application.unregisterWaitForInteraction(this.onInteraction);
46
35
  this.removeListenerIfItExists();
src/engine-components/AudioSource.ts CHANGED
@@ -3,7 +3,6 @@
3
3
 
4
4
  import { isDevEnvironment } from "../engine/debug/index.js";
5
5
  import { Application, ApplicationEvents } from "../engine/engine_application.js";
6
- import { findObjectOfType } from "../engine/engine_components.js";
7
6
  import { Mathf } from "../engine/engine_math.js";
8
7
  import { serializable } from "../engine/engine_serialization_decorator.js";
9
8
  import { DeviceUtilities, getParam } from "../engine/engine_utils.js";
@@ -14,121 +13,84 @@
14
13
  const debug = getParam("debugaudio");
15
14
 
16
15
  /**
17
- * Defines how audio volume attenuates over distance from the listener.
16
+ * The AudioRolloffMode enum describes different ways that audio can attenuate with distance.
18
17
  */
19
18
  export enum AudioRolloffMode {
20
- /**
21
- * Logarithmic rolloff provides a natural, real-world attenuation where volume decreases
22
- * exponentially with distance.
23
- */
19
+ /// <summary>
20
+ /// <para>Use this mode when you want a real-world rolloff.</para>
21
+ /// </summary>
24
22
  Logarithmic = 0,
25
-
26
- /**
27
- * Linear rolloff provides a straightforward volume reduction that decreases at a constant
28
- * rate with distance.
29
- */
23
+ /// <summary>
24
+ /// <para>Use this mode when you want to lower the volume of your sound over the distance.</para>
25
+ /// </summary>
30
26
  Linear = 1,
31
-
32
- /**
33
- * Custom rolloff allows for defining specialized distance-based attenuation curves.
34
- * Note: Custom rolloff is not fully implemented in this version.
35
- */
27
+ /// <summary>
28
+ /// <para>Use this when you want to use a custom rolloff.</para>
29
+ /// </summary>
36
30
  Custom = 2,
37
31
  }
38
32
 
39
33
 
40
- /**
41
- * Plays audio clips in the scene, with support for spatial positioning.
42
- *
43
- * The AudioSource component can play audio files or media streams with
44
- * options for spatial blending, volume control, looping, and more.
45
- *
46
- * When a page loses visibility (tab becomes inactive), audio will automatically
47
- * pause unless {@link playInBackground} is set to true. On mobile devices, audio always
48
- * pauses regardless of this setting. When the page becomes visible again,
49
- * previously playing audio will resume.
50
- *
51
- * AudioSource also responds to application mute state changes. When the application
52
- * is muted, the volume is set to 0. When unmuted, the volume
53
- * returns to its previous value.
54
- *
34
+ /** The AudioSource can be used to play audio in the scene.
35
+ * Use `clip` to set the audio file to play.
55
36
  * @category Multimedia
56
37
  * @group Components
57
38
  */
58
39
  export class AudioSource extends Behaviour {
59
40
 
60
- /**
61
- * Checks if the user has interacted with the page to allow audio playback.
62
- * Audio playback often requires a user gesture first due to browser autoplay policies.
63
- * This is the same as calling {@link Application.userInteractionRegistered}.
64
- *
65
- * @returns Whether user interaction has been registered to allow audio playback
41
+ /** Check if the user has interacted with the page to allow audio playback.
42
+ * Internally calling {@link Application.userInteractionRegistered}
66
43
  */
67
44
  public static get userInteractionRegistered(): boolean {
68
45
  return Application.userInteractionRegistered;
69
46
  }
70
47
 
71
- /**
72
- * Registers a callback that will be executed once the user has interacted with the page,
73
- * allowing audio playback to begin.
74
- * This is the same as calling {@link Application.registerWaitForInteraction}.
75
- *
76
- * @param cb - The callback function to execute when user interaction is registered
48
+ /** Register a callback that is called when the user has interacted with the page to allow audio playback.
49
+ * Internally calling {@link Application.registerWaitForInteraction}
77
50
  */
78
51
  public static registerWaitForAllowAudio(cb: Function) {
79
52
  Application.registerWaitForInteraction(cb);
80
53
  }
81
54
 
82
55
  /**
83
- * The audio clip to play. Can be a URL string pointing to an audio file or a {@link MediaStream} object.
56
+ * The audio clip to play. Can be a string (URL) or a MediaStream.
84
57
  */
85
58
  @serializable(URL)
86
59
  clip: string | MediaStream = "";
87
60
 
88
61
  /**
89
- * When true, the audio will automatically start playing when the component is enabled.
90
- * When false, you must call play() manually to start audio playback.
62
+ * If true, the audio source will start playing as soon as the scene starts.
63
+ * If false, you can call play() to start the audio.
91
64
  * @default false
92
- */
65
+ */
93
66
  @serializable()
94
67
  playOnAwake: boolean = false;
95
68
 
96
69
  /**
97
- * When true, the audio clip will be loaded during initialization rather than when play() is called.
98
- * This can reduce playback delay but increases initial loading time.
70
+ * If true, the audio source will start loading the audio clip as soon as the scene starts.
71
+ * If false, the audio clip will be loaded when play() is called.
99
72
  * @default false
100
73
  */
101
74
  @serializable()
102
75
  preload: boolean = false;
103
76
 
104
77
  /**
105
- * When true, audio will continue playing when the browser tab loses focus.
106
- * When false, audio will pause when the tab is minimized or not active.
78
+ * When true, the audio will play in the background. This means it will continue playing if the browser tab is not focused/active or minimized
107
79
  * @default true
108
80
  */
109
81
  @serializable()
110
82
  playInBackground: boolean = true;
111
83
 
112
84
  /**
113
- * Indicates whether the audio is currently playing.
114
- *
115
- * @returns True if the audio is playing, false otherwise
85
+ * If true, the audio is currently playing.
116
86
  */
117
87
  get isPlaying(): boolean { return this.sound?.isPlaying ?? false; }
118
88
 
119
- /**
120
- * The total duration of the current audio clip in seconds.
121
- *
122
- * @returns Duration in seconds or undefined if no clip is loaded
123
- */
89
+ /** The duration of the audio clip in seconds. */
124
90
  get duration() {
125
91
  return this.sound?.buffer?.duration;
126
92
  }
127
-
128
- /**
129
- * The current playback position as a normalized value between 0 and 1.
130
- * Can be set to seek to a specific position in the audio.
131
- */
93
+ /** The current time of the audio clip in 0-1 range. */
132
94
  get time01() {
133
95
  const duration = this.duration;
134
96
  if (duration && this.sound) {
@@ -144,8 +106,7 @@
144
106
  }
145
107
 
146
108
  /**
147
- * The current playback position in seconds.
148
- * Can be set to seek to a specific time in the audio.
109
+ * The current time of the audio clip in seconds.
149
110
  */
150
111
  get time(): number { return this.sound?.source ? (this.sound.source?.context.currentTime - this._lastContextTime + this.sound.offset) : 0; }
151
112
  set time(val: number) {
@@ -160,8 +121,8 @@
160
121
  }
161
122
 
162
123
  /**
163
- * When true, the audio will repeat after reaching the end.
164
- * When false, audio will play once and stop.
124
+ * If true, the audio source will loop the audio clip.
125
+ * If false, the audio clip will play once.
165
126
  * @default false
166
127
  */
167
128
  @serializable()
@@ -173,12 +134,10 @@
173
134
  this._loop = val;
174
135
  if (this.sound) this.sound.setLoop(val);
175
136
  }
176
-
177
- /**
178
- * Controls how the audio is positioned in space.
179
- * Values range from 0 (2D, non-positional) to 1 (fully 3D positioned).
180
- * Note: 2D playback is not fully supported in the current implementation.
181
- */
137
+ /** Can be used to play the audio clip in 2D or 3D space.
138
+ * 2D Playback is currently not fully supported.
139
+ * 0 = 2D, 1 = 3D
140
+ * */
182
141
  @serializable()
183
142
  get spatialBlend(): number {
184
143
  return this._spatialBlend;
@@ -188,11 +147,6 @@
188
147
  this._spatialBlend = val;
189
148
  this._needUpdateSpatialDistanceSettings = true;
190
149
  }
191
-
192
- /**
193
- * The minimum distance from the audio source at which the volume starts to attenuate.
194
- * Within this radius, the audio plays at full volume regardless of distance.
195
- */
196
150
  @serializable()
197
151
  get minDistance(): number {
198
152
  return this._minDistance;
@@ -202,11 +156,6 @@
202
156
  this._minDistance = val;
203
157
  this._needUpdateSpatialDistanceSettings = true;
204
158
  }
205
-
206
- /**
207
- * The maximum distance from the audio source beyond which the volume no longer decreases.
208
- * This defines the outer limit of the attenuation curve.
209
- */
210
159
  @serializable()
211
160
  get maxDistance(): number {
212
161
  return this._maxDistance;
@@ -221,11 +170,6 @@
221
170
  private _minDistance: number = 1;
222
171
  private _maxDistance: number = 100;
223
172
 
224
- /**
225
- * Controls the overall volume/loudness of the audio.
226
- * Values range from 0 (silent) to 1 (full volume).
227
- * @default 1
228
- */
229
173
  @serializable()
230
174
  get volume(): number { return this._volume; }
231
175
  set volume(val: number) {
@@ -237,12 +181,6 @@
237
181
  }
238
182
  private _volume: number = 1;
239
183
 
240
- /**
241
- * Controls the playback rate (speed) of the audio.
242
- * Values greater than 1 increase speed, values less than 1 decrease it.
243
- * This affects both speed and pitch of the audio.
244
- * @default 1
245
- */
246
184
  @serializable()
247
185
  set pitch(val: number) {
248
186
  if (this.sound) this.sound.setPlaybackRate(val);
@@ -251,11 +189,6 @@
251
189
  return this.sound ? this.sound.getPlaybackRate() : 1;
252
190
  }
253
191
 
254
- /**
255
- * Determines how audio volume decreases with distance from the listener.
256
- * @default AudioRolloffMode.Logarithmic
257
- * @see {@link AudioRolloffMode}
258
- */
259
192
  @serializable()
260
193
  rollOffMode: AudioRolloffMode = 0;
261
194
 
@@ -271,21 +204,10 @@
271
204
  private _lastClipStartedLoading: string | MediaStream | null = null;
272
205
  private _audioElement: HTMLAudioElement | null = null;
273
206
 
274
- /**
275
- * Returns the underlying {@link PositionalAudio} object, creating it if necessary.
276
- * The audio source needs a user interaction to be initialized due to browser autoplay policies.
277
- *
278
- * @returns The three.js PositionalAudio object or null if unavailable
279
- */
280
207
  public get Sound(): PositionalAudio | null {
281
208
  if (!this.sound && AudioSource.userInteractionRegistered) {
282
- // Get or create an audiolistener in the scene
283
- let listener = this.gameObject.getComponent(AudioListener) // AudioListener on AudioSource?
284
- ?? this.context.mainCamera.getComponent(AudioListener) // AudioListener on current main camera?
285
- ?? findObjectOfType(AudioListener, this.context, false); // Active AudioListener in scene?
286
-
287
- if (!listener && this.context.mainCamera) listener = this.context.mainCamera.addComponent(AudioListener);
288
-
209
+ let listener = GameObject.getComponent(this.context.mainCamera, AudioListener) ?? GameObject.findObjectOfType(AudioListener, this.context);
210
+ if (!listener && this.context.mainCamera) listener = GameObject.addComponent(this.context.mainCamera, AudioListener);
289
211
  if (listener?.listener) {
290
212
  this.sound = new PositionalAudio(listener.listener);
291
213
  this.gameObject?.add(this.sound);
@@ -328,19 +250,9 @@
328
250
  // }
329
251
  // }
330
252
 
331
- /**
332
- * Indicates whether the audio source is queued to play when possible.
333
- * This may be true before user interaction has been registered.
334
- *
335
- * @returns Whether the audio source intends to play
336
- */
337
253
  public get ShouldPlay(): boolean { return this.shouldPlay; }
338
254
 
339
- /**
340
- * Returns the Web Audio API context associated with this audio source.
341
- *
342
- * @returns The {@link AudioContext} or null if not available
343
- */
255
+ /** Get the audio context from the Sound */
344
256
  public get audioContext() {
345
257
  return this.sound?.context;
346
258
  }
@@ -432,9 +344,6 @@
432
344
  if (sound.isPlaying)
433
345
  sound.stop();
434
346
 
435
- // const panner = sound.panner;
436
- // panner.coneOuterGain = 1;
437
- // sound.setDirectionalCone(360, 360, 1);
438
347
  // const src = sound.context.createBufferSource();
439
348
  // src.buffer = sound.buffer;
440
349
  // src.connect(sound.panner);
@@ -515,12 +424,7 @@
515
424
  }
516
425
  }
517
426
 
518
- /**
519
- * Plays the audio clip or media stream.
520
- * If no argument is provided, plays the currently assigned clip.
521
- *
522
- * @param clip - Optional audio clip or {@link MediaStream} to play
523
- */
427
+ /** Play a audioclip or mediastream */
524
428
  play(clip: string | MediaStream | undefined = undefined) {
525
429
  // use audio source's clip when no clip is passed in
526
430
  if (!clip && this.clip)
@@ -579,8 +483,7 @@
579
483
  }
580
484
 
581
485
  /**
582
- * Pauses audio playback while maintaining the current position.
583
- * Use play() to resume from the paused position.
486
+ * Pause the audio
584
487
  */
585
488
  pause() {
586
489
  if (debug) console.log("Pause", this);
@@ -594,8 +497,7 @@
594
497
  }
595
498
 
596
499
  /**
597
- * Stops audio playback completely and resets the playback position to the beginning.
598
- * Unlike pause(), calling play() after stop() will start from the beginning.
500
+ * Stop the audio and reset the time to 0
599
501
  */
600
502
  stop() {
601
503
  if (debug) console.log("Pause", this);
src/engine-components/AvatarLoader.ts CHANGED
@@ -10,37 +10,17 @@
10
10
 
11
11
  const debug = utils.getParam("debugavatar");
12
12
 
13
- /**
14
- * Represents an avatar model with head and hands references.
15
- * Used for representing characters in 3D space.
16
- */
17
13
  export class AvatarModel {
18
- /** The root object of the avatar model */
19
14
  root: Object3D;
20
- /** The head object of the avatar model */
21
15
  head: Object3D;
22
- /** The left hand object of the avatar model, if available */
23
16
  leftHand: Object3D | null;
24
- /** The right hand object of the avatar model, if available */
25
17
  rigthHand: Object3D | null;
26
18
 
27
19
 
28
- /**
29
- * Checks if the avatar model has a valid configuration.
30
- * An avatar is considered valid if it has a head.
31
- * @returns Whether the avatar has a valid setup
32
- */
33
20
  get isValid(): boolean {
34
21
  return this.head !== null && this.head !== undefined;
35
22
  }
36
23
 
37
- /**
38
- * Creates a new avatar model.
39
- * @param root The root object of the avatar
40
- * @param head The head object of the avatar
41
- * @param leftHand The left hand object of the avatar
42
- * @param rigthHand The right hand object of the avatar
43
- */
44
24
  constructor(root: Object3D, head: Object3D, leftHand: Object3D | null, rigthHand: Object3D | null) {
45
25
  this.root = root;
46
26
  this.head = head;
@@ -53,25 +33,12 @@
53
33
  }
54
34
  }
55
35
 
56
- /**
57
- * Handles loading and instantiating avatar models from various sources.
58
- * Provides functionality to find and extract important parts of an avatar (head, hands).
59
- *
60
- * Debug mode can be enabled with the URL parameter `?debugavatar`,
61
- * which will log detailed information about avatar loading and configuration.
62
- */
63
36
  export class AvatarLoader {
64
37
 
65
38
  private readonly avatarRegistryUrl: string | null = null;
66
39
  // private loader: GLTFLoader | null;
67
40
  // private avatarModelCache: Map<string, AvatarModel | null> = new Map<string, AvatarModel | null>();
68
41
 
69
- /**
70
- * Retrieves or creates a new avatar instance from an ID or existing Object3D.
71
- * @param context The application context
72
- * @param avatarId Either a string ID to load an avatar or an existing Object3D to use as avatar
73
- * @returns Promise resolving to an AvatarModel if successful, or null if failed
74
- */
75
42
  public async getOrCreateNewAvatarInstance(context: Context, avatarId: string | Object3D): Promise<AvatarModel | null> {
76
43
 
77
44
  if (!avatarId) {
@@ -109,12 +76,6 @@
109
76
  }
110
77
 
111
78
 
112
- /**
113
- * Loads an avatar model from a file or registry using the provided ID.
114
- * @param context The engine context
115
- * @param avatarId The ID of the avatar to load
116
- * @returns Promise resolving to the loaded avatar's Object3D, or null if failed
117
- */
118
79
  private async loadAvatar(context: Context, avatarId: string): Promise<Object3D | null> {
119
80
 
120
81
  console.assert(avatarId !== undefined && avatarId !== null && typeof avatarId === "string", "Avatar id must not be null");
@@ -179,20 +140,11 @@
179
140
  });
180
141
  }
181
142
 
182
- /**
183
- * Caches an avatar model for reuse.
184
- * @param _id The ID to associate with the model
185
- * @param _model The avatar model to cache
186
- */
187
143
  private cacheModel(_id: string, _model: AvatarModel) {
188
144
  // this.avatarModelCache.set(id, model);
189
145
  }
190
146
 
191
- /**
192
- * Analyzes an Object3D to find avatar parts (head, hands) based on naming conventions.
193
- * @param obj The Object3D to search for avatar parts
194
- * @returns A structured AvatarModel with references to found parts
195
- */
147
+ // TODO this should be burned to the ground once 🤞 we have proper extras that define object relations.
196
148
  private findAvatar(obj: Object3D): AvatarModel {
197
149
 
198
150
  const root: Object3D = obj;
@@ -223,12 +175,7 @@
223
175
  return model;
224
176
  }
225
177
 
226
- /**
227
- * Recursively searches for an avatar part by name within an Object3D hierarchy.
228
- * @param obj The Object3D to search within
229
- * @param searchString Array of strings that should all be present in the object name
230
- * @returns The found Object3D part or null if not found
231
- */
178
+
232
179
  private findAvatarPart(obj: Object3D, searchString: string[]): Object3D | null {
233
180
 
234
181
  const name = obj.name.toLowerCase();
@@ -249,12 +196,6 @@
249
196
  return null;
250
197
  }
251
198
 
252
- /**
253
- * Handles HTTP response errors from avatar loading operations.
254
- * @param response The fetch API response to check
255
- * @returns The response if it was ok
256
- * @throws Error with status text if response was not ok
257
- */
258
199
  private handleCustomAvatarErrors(response) {
259
200
  if (!response.ok) {
260
201
  throw Error(response.statusText);
src/engine-components/AxesHelper.ts CHANGED
@@ -5,37 +5,20 @@
5
5
  import { Behaviour } from "./Component.js";
6
6
 
7
7
  /**
8
- * Component that visualizes the axes of an object in the scene.
9
- * Renders colored lines representing the X (red), Y (green) and Z (blue) axes.
8
+ * AxesHelper is a component that displays the axes of the object in the scene.
10
9
  * @category Helpers
11
10
  * @group Components
12
11
  */
13
12
  export class AxesHelper extends Behaviour {
14
- /**
15
- * The length of each axis line in scene units.
16
- */
17
13
  @serializable()
18
14
  public length: number = 1;
19
-
20
- /**
21
- * Whether the axes should be occluded by objects in the scene.
22
- * When set to false, axes will always appear on top regardless of their depth.
23
- */
24
15
  @serializable()
25
16
  public depthTest: boolean = true;
26
-
27
- /**
28
- * When true, this helper will only be visible if the debug flag `?gizmos` is enabled.
29
- */
30
17
  @serializable()
31
18
  public isGizmo: boolean = false;
32
19
 
33
20
  private _axes: _AxesHelper | null = null;
34
21
 
35
- /**
36
- * Creates and adds the axes visualization to the scene when the component is enabled.
37
- * If marked as a gizmo, it will only be shown when gizmos are enabled in the global parameters.
38
- */
39
22
  onEnable(): void {
40
23
  if (this.isGizmo && !params.showGizmos) return;
41
24
  if (!this._axes)
@@ -50,9 +33,6 @@
50
33
  }
51
34
  }
52
35
 
53
- /**
54
- * Removes the axes visualization from the scene when the component is disabled.
55
- */
56
36
  onDisable(): void {
57
37
  if (!this._axes) return;
58
38
  this.gameObject.remove(this._axes);
src/engine-components/BoxHelperComponent.ts CHANGED
@@ -9,17 +9,11 @@
9
9
  const debug = getParam("debugboxhelper");
10
10
 
11
11
  /**
12
- * A component that creates a bounding box around an object and provides intersection testing functionality.
13
- *
14
- * Debug mode can be enabled with the URL parameter `?debugboxhelper`, which will visualize intersection tests.
15
- * Helper visualization can be enabled with the URL parameter `?gizmos`.
16
- *
17
12
  * @category Helpers
18
13
  * @group Components
19
14
  */
20
15
  export class BoxHelperComponent extends Behaviour {
21
16
 
22
- /** The bounding box for this component */
23
17
  private box: Box3 | null = null;
24
18
  private static testBox: Box3 = new Box3();
25
19
  private _lastMatrixUpdateFrame: number = -1;
@@ -27,11 +21,6 @@
27
21
  private static _size: Vector3 = new Vector3(.01, .01, .01);
28
22
  private static _emptyObjectSize: Vector3 = new Vector3(.01, .01, .01);
29
23
 
30
- /**
31
- * Tests if an object intersects with this helper's bounding box
32
- * @param obj The object to test for intersection
33
- * @returns True if objects intersect, false if not, undefined if the provided object is invalid
34
- */
35
24
  public isInBox(obj: Object3D): boolean | undefined {
36
25
  if (!obj) return undefined;
37
26
 
@@ -55,21 +44,11 @@
55
44
  return intersects;
56
45
  }
57
46
 
58
- /**
59
- * Tests if this helper's bounding box intersects with another box
60
- * @param box The {@link Box3} to test for intersection
61
- * @returns True if boxes intersect, false otherwise
62
- */
63
47
  public intersects(box: Box3): boolean {
64
48
  if (!box) return false;
65
49
  return this.updateBox(false).intersectsBox(box);
66
50
  }
67
51
 
68
- /**
69
- * Updates the helper's bounding box based on the gameObject's position and scale
70
- * @param force Whether to force an update regardless of frame count
71
- * @returns The updated {@link Box3}
72
- */
73
52
  public updateBox(force: boolean = false): Box3 {
74
53
  if (!this.box) {
75
54
  this.box = new Box3();
@@ -95,11 +74,6 @@
95
74
  this.box = null;
96
75
  }
97
76
 
98
- /**
99
- * Creates and displays a visual wireframe representation of this box helper
100
- * @param col Optional color for the wireframe. If not provided, uses default color
101
- * @param force If true, shows the helper even if gizmos are disabled
102
- */
103
77
  public showHelper(col: ColorRepresentation | null = null, force: boolean = false) {
104
78
  if (!gizmos && !force) return;
105
79
  if (this._helper) {
src/engine-components/Camera.ts CHANGED
@@ -13,11 +13,8 @@
13
13
  import { Behaviour, GameObject } from "./Component.js";
14
14
  import { OrbitControls } from "./OrbitControls.js";
15
15
 
16
- /**
17
- * The ClearFlags enum is used to determine how the camera clears the background
18
- */
16
+ /** The ClearFlags enum is used to determine how the camera clears the background */
19
17
  export enum ClearFlags {
20
- /** Don't clear the background */
21
18
  None = 0,
22
19
  /** Clear the background with a skybox */
23
20
  Skybox = 1,
@@ -31,28 +28,16 @@
31
28
  const debugscreenpointtoray = getParam("debugscreenpointtoray");
32
29
 
33
30
  /**
34
- * Camera component that handles rendering from a specific viewpoint in the scene.
35
- * Supports both perspective and orthographic cameras with various rendering options.
36
- * Internally, this component uses {@link PerspectiveCamera} and {@link OrthographicCamera} three.js objects.
37
- *
38
31
  * @category Camera Controls
39
32
  * @group Components
40
33
  */
41
34
  export class Camera extends Behaviour implements ICamera {
42
35
 
43
- /**
44
- * Returns whether this component is a camera
45
- * @returns {boolean} Always returns true
46
- */
47
36
  get isCamera() {
48
37
  return true;
49
38
  }
50
39
 
51
- /**
52
- * Gets or sets the camera's aspect ratio (width divided by height).
53
- * For perspective cameras, this directly affects the camera's projection matrix.
54
- * When set, automatically updates the projection matrix.
55
- */
40
+ /** The camera's aspect ratio (width divided by height) if it is a perspective camera */
56
41
  get aspect(): number {
57
42
  if (this._cam instanceof PerspectiveCamera) return this._cam.aspect;
58
43
  return (this.context.domWidth / this.context.domHeight);
@@ -66,11 +51,7 @@
66
51
  }
67
52
  }
68
53
  }
69
-
70
- /**
71
- * Gets or sets the camera's field of view in degrees for perspective cameras.
72
- * When set, automatically updates the projection matrix.
73
- */
54
+ /** The camera's field of view in degrees if it is a perspective camera. Calls updateProjectionMatrix when set */
74
55
  get fieldOfView(): number | undefined {
75
56
  if (this._cam instanceof PerspectiveCamera) {
76
57
  return this._cam.fov;
@@ -93,11 +74,7 @@
93
74
  }
94
75
  }
95
76
 
96
- /**
97
- * Gets or sets the camera's near clipping plane distance.
98
- * Objects closer than this distance won't be rendered.
99
- * When set, automatically updates the projection matrix.
100
- */
77
+ /** The camera's near clipping plane. Calls updateProjectionMatrix when set */
101
78
  get nearClipPlane(): number { return this._nearClipPlane; }
102
79
  @serializable()
103
80
  set nearClipPlane(val) {
@@ -110,11 +87,7 @@
110
87
  }
111
88
  private _nearClipPlane: number = 0.1;
112
89
 
113
- /**
114
- * Gets or sets the camera's far clipping plane distance.
115
- * Objects farther than this distance won't be rendered.
116
- * When set, automatically updates the projection matrix.
117
- */
90
+ /** The camera's far clipping plane. Calls updateProjectionMatrix when set */
118
91
  get farClipPlane(): number { return this._farClipPlane; }
119
92
  @serializable()
120
93
  set farClipPlane(val) {
@@ -128,8 +101,7 @@
128
101
  private _farClipPlane: number = 1000;
129
102
 
130
103
  /**
131
- * Applies both the camera's near and far clipping planes and updates the projection matrix.
132
- * This ensures rendering occurs only within the specified distance range.
104
+ * Applys both the camera's near and far plane and calls updateProjectionMatrix on the camera.
133
105
  */
134
106
  applyClippingPlane() {
135
107
  if (this._cam) {
@@ -139,10 +111,7 @@
139
111
  }
140
112
  }
141
113
 
142
- /**
143
- * Gets or sets the camera's clear flags that determine how the background is rendered.
144
- * Options include skybox, solid color, or transparent background.
145
- */
114
+ /** The camera's clear flags - determines if the background is a skybox or a solid color or transparent */
146
115
  @serializable()
147
116
  public get clearFlags(): ClearFlags {
148
117
  return this._clearFlags;
@@ -152,31 +121,18 @@
152
121
  this._clearFlags = val;
153
122
  this.applyClearFlagsIfIsActiveCamera();
154
123
  }
155
-
156
- /**
157
- * Determines if the camera should use orthographic projection instead of perspective.
158
- */
159
124
  @serializable()
160
125
  public orthographic: boolean = false;
161
-
162
- /**
163
- * The size of the orthographic camera's view volume when in orthographic mode.
164
- * Larger values show more of the scene.
165
- */
166
126
  @serializable()
167
127
  public orthographicSize: number = 5;
168
128
 
169
- /**
170
- * Controls the transparency level of the camera background in AR mode on supported devices.
171
- * Value from 0 (fully transparent) to 1 (fully opaque).
172
- */
173
129
  @serializable()
174
130
  public ARBackgroundAlpha: number = 0;
175
131
 
176
132
  /**
177
- * Gets or sets the layers mask that determines which objects this camera will render.
178
- * Uses the {@link https://threejs.org/docs/#api/en/core/Layers.mask|three.js layers mask} convention.
179
- */
133
+ * The [`mask`](https://threejs.org/docs/#api/en/core/Layers.mask) value of the three camera object layers
134
+ * If you want to just see objects on one layer (e.g. layer 2) then you can use `cullingLayer = 2` on this camera component instead
135
+ */
180
136
  @serializable()
181
137
  public set cullingMask(val: number) {
182
138
  this._cullingMask = val;
@@ -190,19 +146,14 @@
190
146
  }
191
147
  private _cullingMask: number = 0xffffffff;
192
148
 
193
- /**
194
- * Sets only a specific layer to be active for rendering by this camera.
195
- * This is equivalent to calling `layers.set(val)` on the three.js camera object.
196
- * @param val The layer index to set active
197
- */
149
+ /** Set only a specific layer active to be rendered by the camera.
150
+ * This is equivalent to calling `layers.set(val)`
151
+ **/
198
152
  public set cullingLayer(val: number) {
199
153
  this.cullingMask = (1 << val | 0) >>> 0;
200
154
  }
201
155
 
202
- /**
203
- * Gets or sets the blurriness of the skybox background.
204
- * Values range from 0 (sharp) to 1 (maximum blur).
205
- */
156
+ /** The blurriness of the background texture (when using a skybox) */
206
157
  @serializable()
207
158
  public set backgroundBlurriness(val: number | undefined) {
208
159
  if (val === this._backgroundBlurriness) return;
@@ -217,10 +168,7 @@
217
168
  }
218
169
  private _backgroundBlurriness?: number = undefined;
219
170
 
220
- /**
221
- * Gets or sets the intensity of the skybox background.
222
- * Values range from 0 (dark) to 10 (very bright).
223
- */
171
+ /** The intensity of the background texture (when using a skybox) */
224
172
  @serializable()
225
173
  public set backgroundIntensity(val: number | undefined) {
226
174
  if (val === this._backgroundIntensity) return;
@@ -235,10 +183,7 @@
235
183
  }
236
184
  private _backgroundIntensity?: number = undefined;
237
185
 
238
- /**
239
- * Gets or sets the rotation of the skybox background.
240
- * Controls the orientation of the environment map.
241
- */
186
+ /** the rotation of the background texture (when using a skybox) */
242
187
  @serializable(Euler)
243
188
  public set backgroundRotation(val: Euler | undefined) {
244
189
  if (val === this._backgroundRotation) return;
@@ -253,10 +198,7 @@
253
198
  }
254
199
  private _backgroundRotation?: Euler = undefined;
255
200
 
256
- /**
257
- * Gets or sets the intensity of the environment lighting.
258
- * Controls how strongly the environment map affects scene lighting.
259
- */
201
+ /** The intensity of the environment map */
260
202
  @serializable()
261
203
  public set environmentIntensity(val: number | undefined) {
262
204
  this._environmentIntensity = val;
@@ -266,10 +208,7 @@
266
208
  }
267
209
  private _environmentIntensity?: number = undefined;
268
210
 
269
- /**
270
- * Gets or sets the background color of the camera when {@link ClearFlags} is set to {@link ClearFlags.SolidColor}.
271
- * The alpha component controls transparency.
272
- */
211
+ /** The background color of the camera when {@link ClearFlags} are set to `SolidColor` */
273
212
  @serializable(RGBAColor)
274
213
  public get backgroundColor(): RGBAColor | null {
275
214
  return this._backgroundColor ?? null;
@@ -286,10 +225,9 @@
286
225
  this.applyClearFlagsIfIsActiveCamera();
287
226
  }
288
227
 
289
- /**
290
- * Gets or sets the texture that the camera should render to instead of the screen.
291
- * Useful for creating effects like mirrors, portals or custom post processing.
292
- */
228
+ /** The texture that the camera should render to
229
+ * It can be used to render to a {@link Texture} instead of the screen.
230
+ */
293
231
  @serializable(RenderTexture)
294
232
  public set targetTexture(rt: RenderTexture | null) {
295
233
  this._targetTexture = rt;
@@ -306,17 +244,16 @@
306
244
  private _skybox?: CameraSkybox;
307
245
 
308
246
  /**
309
- * Gets the three.js camera object. Creates one if it doesn't exist yet.
310
- * @returns {PerspectiveCamera | OrthographicCamera} The three.js camera object
311
- * @deprecated Use {@link threeCamera} instead
247
+ * Get the three.js camera object. This will create a camera if it does not exist yet.
248
+ * @returns {PerspectiveCamera | OrthographicCamera} the three camera
249
+ * @deprecated use {@link threeCamera} instead
312
250
  */
313
251
  public get cam(): PerspectiveCamera | OrthographicCamera {
314
252
  return this.threeCamera;
315
253
  }
316
-
317
254
  /**
318
- * Gets the three.js camera object. Creates one if it doesn't exist yet.
319
- * @returns {PerspectiveCamera | OrthographicCamera} The three.js camera object
255
+ * Get the three.js camera object. This will create a camera if it does not exist yet.
256
+ * @returns {PerspectiveCamera | OrthographicCamera} the three camera
320
257
  */
321
258
  public get threeCamera(): PerspectiveCamera | OrthographicCamera {
322
259
  if (this.activeAndEnabled)
@@ -326,16 +263,6 @@
326
263
 
327
264
  private static _origin: Vector3 = new Vector3();
328
265
  private static _direction: Vector3 = new Vector3();
329
-
330
- /**
331
- * Converts screen coordinates to a ray in world space.
332
- * Useful for implementing picking or raycasting from screen to world.
333
- *
334
- * @param x The x screen coordinate
335
- * @param y The y screen coordinate
336
- * @param ray Optional ray object to reuse instead of creating a new one
337
- * @returns {Ray} A ray originating from the camera position pointing through the screen point
338
- */
339
266
  public screenPointToRay(x: number, y: number, ray?: Ray): Ray {
340
267
  const cam = this.threeCamera;
341
268
  const origin = Camera._origin;
@@ -358,12 +285,9 @@
358
285
  }
359
286
 
360
287
  private _frustum?: Frustum;
361
-
362
288
  /**
363
- * Gets the camera's view frustum for culling and visibility checks.
364
- * Creates the frustum if it doesn't exist and returns it.
365
- *
366
- * @returns {Frustum} The camera's view frustum
289
+ * Get a frustum - it will be created the first time this method is called and updated every frame in onBeforeRender when it exists.
290
+ * You can also manually update it using the updateFrustum method.
367
291
  */
368
292
  public getFrustum(): Frustum {
369
293
  if (!this._frustum) {
@@ -372,22 +296,13 @@
372
296
  }
373
297
  return this._frustum;
374
298
  }
375
-
376
- /**
377
- * Forces an update of the camera's frustum.
378
- * This is automatically called every frame in onBeforeRender.
379
- */
299
+ /** Force frustum update - note that this also happens automatically every frame in onBeforeRender */
380
300
  public updateFrustum() {
381
301
  if (!this._frustum) this._frustum = new Frustum();
382
302
  this._frustum.setFromProjectionMatrix(this.getProjectionScreenMatrix(this._projScreenMatrix, true), this.context.renderer.coordinateSystem);
383
303
  }
384
-
385
304
  /**
386
- * Gets this camera's projection-screen matrix.
387
- *
388
- * @param target Matrix4 object to store the result in
389
- * @param forceUpdate Whether to force recalculation of the matrix
390
- * @returns {Matrix4} The requested projection screen matrix
305
+ * @returns {Matrix4} this camera's projection screen matrix.
391
306
  */
392
307
  public getProjectionScreenMatrix(target: Matrix4, forceUpdate?: boolean) {
393
308
  if (forceUpdate) {
@@ -398,6 +313,7 @@
398
313
  }
399
314
  private readonly _projScreenMatrix = new Matrix4();
400
315
 
316
+
401
317
  /** @internal */
402
318
  awake() {
403
319
  if (debugscreenpointtoray) {
@@ -466,9 +382,8 @@
466
382
  }
467
383
 
468
384
  /**
469
- * Creates a three.js camera object if it doesn't exist yet and sets its properties.
470
- * This is called internally when accessing the {@link threeCamera} property.
471
- */
385
+ * Creates a {@link PerspectiveCamera} if it does not exist yet and set the camera's properties. This is internally also called when accessing the {@link cam} property.
386
+ **/
472
387
  buildCamera() {
473
388
  if (this._cam) return;
474
389
 
@@ -513,21 +428,13 @@
513
428
  }
514
429
  }
515
430
 
516
- /**
517
- * Applies clear flags if this is the active main camera.
518
- * @param opts Options for applying clear flags
519
- */
520
431
  applyClearFlagsIfIsActiveCamera(opts?: { applySkybox: boolean }) {
521
432
  if (this.context.mainCameraComponent === this) {
522
433
  this.applyClearFlags(opts);
523
434
  }
524
435
  }
525
436
 
526
- /**
527
- * Applies this camera's clear flags and related settings to the renderer.
528
- * This controls how the background is rendered (skybox, solid color, transparent).
529
- * @param opts Options for applying clear flags
530
- */
437
+ /** Apply this camera's clear flags and related settings to the renderer */
531
438
  applyClearFlags(opts?: { applySkybox: boolean }) {
532
439
  if (!this._cam) {
533
440
  if (debug) console.log("Camera does not exist (apply clear flags)")
@@ -596,7 +503,7 @@
596
503
  }
597
504
 
598
505
  /**
599
- * Applies the skybox texture to the scene background.
506
+ * Apply the skybox to the scene
600
507
  */
601
508
  applySceneSkybox() {
602
509
  if (!this._skybox)
@@ -604,12 +511,9 @@
604
511
  this._skybox.apply();
605
512
  }
606
513
 
607
- /**
608
- * Determines if the background should be transparent when in passthrough AR mode.
609
- *
610
- * @param context The current rendering context
611
- * @returns {boolean} True when in XR on a pass through device where the background should be invisible
612
- */
514
+ /** Used to determine if the background should be transparent when in pass through AR
515
+ * @returns true when in XR on a pass through device where the background shouldbe invisible
516
+ **/
613
517
  static backgroundShouldBeTransparent(context: Context) {
614
518
  const session = context.renderer.xr?.getSession();
615
519
  if (!session) return false;
@@ -639,10 +543,7 @@
639
543
  }
640
544
  }
641
545
 
642
- /**
643
- * Helper class for managing skybox textures for cameras.
644
- * Handles retrieving and applying skybox textures to the scene.
645
- */
546
+
646
547
  class CameraSkybox {
647
548
 
648
549
  private _camera: Camera;
@@ -654,10 +555,6 @@
654
555
  this._camera = camera;
655
556
  }
656
557
 
657
- /**
658
- * Applies the skybox texture to the scene background.
659
- * Retrieves the texture based on the camera's source ID.
660
- */
661
558
  apply() {
662
559
  this._skybox = this.context.lightmaps.tryGetSkybox(this._camera.sourceId) as Texture;
663
560
  if (!this._skybox) {
@@ -675,16 +572,13 @@
675
572
  }
676
573
  }
677
574
 
678
- /**
679
- * Adds orbit controls to the camera if the freecam URL parameter is enabled.
680
- *
681
- * @param cam The camera to potentially add orbit controls to
682
- */
575
+
683
576
  function handleFreeCam(cam: Camera) {
684
577
  const isFreecam = getParam("freecam");
685
578
  if (isFreecam) {
686
579
  if (cam.context.mainCameraComponent === cam) {
687
580
  GameObject.getOrAddComponent(cam.gameObject, OrbitControls);
688
581
  }
582
+
689
583
  }
690
584
  }
src/engine-components/CameraUtils.ts CHANGED
@@ -13,13 +13,6 @@
13
13
 
14
14
  const debug = getParam("debugmissingcamera");
15
15
 
16
- /**
17
- * Handler for missing camera events. Creates a default fallback camera when no camera is found in the scene.
18
- * Sets up camera properties based on the context and HTML element attributes.
19
- *
20
- * @param evt The context event containing scene and configuration information
21
- * @returns The created camera component
22
- */
23
16
  ContextRegistry.registerCallback(ContextEvent.MissingCamera, (evt) => {
24
17
  if (debug) console.warn("Creating missing camera")
25
18
  const scene = evt.context.scene;
@@ -64,12 +57,6 @@
64
57
  return cam;
65
58
  });
66
59
 
67
- /**
68
- * Handler for context creation events. Checks if camera controls should be added
69
- * to the main camera when the context is created.
70
- *
71
- * @param evt The context creation event containing the context information
72
- */
73
60
  ContextRegistry.registerCallback(ContextEvent.ContextCreated, (evt) => {
74
61
  if (!evt.context.mainCamera) {
75
62
  if (debug) console.log("Will not auto-fit because a default camera exists");
@@ -90,13 +77,6 @@
90
77
  }
91
78
  })
92
79
 
93
- /**
94
- * Creates default orbit camera controls for the specified camera.
95
- * Configures auto-rotation and auto-fit settings based on HTML attributes.
96
- *
97
- * @param context The rendering context
98
- * @param cam Optional camera component to attach controls to (uses main camera if not specified)
99
- */
100
80
  function createDefaultCameraControls(context: IContext, cam?: ICamera) {
101
81
 
102
82
  cam = cam ?? context.mainCameraComponent;
src/engine-components/Collider.ts CHANGED
@@ -1,63 +1,59 @@
1
1
  import { BufferGeometry, Group, Mesh, Object3D, Vector3 } from "three"
2
2
 
3
- import { isDevEnvironment } from "../engine/debug/index.js";
4
3
  import { addComponent } from "../engine/engine_components.js";
5
4
  import { Gizmos } from "../engine/engine_gizmos.js";
6
5
  import type { PhysicsMaterial } from "../engine/engine_physics.types.js";
7
6
  import { serializable } from "../engine/engine_serialization_decorator.js";
8
- import { getBoundingBox } from "../engine/engine_three_utils.js";
7
+ import { getBoundingBox, getWorldScale } from "../engine/engine_three_utils.js";
8
+ // import { IColliderProvider, registerColliderProvider } from "../engine/engine_physics.js";
9
9
  import type { IBoxCollider, ICollider, ISphereCollider } from "../engine/engine_types.js";
10
10
  import { validate } from "../engine/engine_util_decorator.js";
11
- import { getParam, unwatchWrite, watchWrite } from "../engine/engine_utils.js";
11
+ import { unwatchWrite, watchWrite } from "../engine/engine_utils.js";
12
12
  import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js";
13
13
  import { Behaviour } from "./Component.js";
14
14
  import { Rigidbody } from "./RigidBody.js";
15
15
 
16
16
  /**
17
17
  * Collider is the base class for all colliders. A collider is a physical shape that is used to detect collisions with other objects in the scene.
18
- * Colliders are used in combination with a {@link Rigidbody} to create physical interactions between objects.
18
+ * Colliders are used in combination with a Rigidbody to create physical interactions between objects.
19
19
  * Colliders are registered with the physics engine when they are enabled and removed when they are disabled.
20
20
  * @category Physics
21
21
  * @group Components
22
22
  */
23
23
  export class Collider extends Behaviour implements ICollider {
24
24
 
25
- /**
26
- * Identifies this component as a collider.
27
- * @internal
28
- */
25
+ /** @internal */
29
26
  get isCollider(): any {
30
27
  return true;
31
28
  }
32
29
 
33
30
  /**
34
- * The {@link Rigidbody} that this collider is attached to. This handles the physics simulation for this collider.
31
+ * The Rigidbody that this collider is attached to.
35
32
  */
36
33
  @serializable(Rigidbody)
37
34
  attachedRigidbody: Rigidbody | null = null;
38
35
 
39
36
  /**
40
37
  * When `true` the collider will not be used for collision detection but will still trigger events.
41
- * Trigger colliders can trigger events when other colliders enter their space, without creating a physical response/collision.
42
38
  */
43
39
  @serializable()
44
40
  isTrigger: boolean = false;
45
41
 
46
42
  /**
47
- * The physics material that defines physical properties of the collider such as friction and bounciness.
43
+ * The physics material that is used for the collider. This material defines physical properties of the collider such as friction and bounciness.
48
44
  */
49
45
  @serializable()
50
46
  sharedMaterial?: PhysicsMaterial;
51
47
 
52
48
  /**
53
- * The layers that this collider belongs to. Used for filtering collision detection.
54
- * @default [0]
49
+ * The layers that the collider is assigned to.
55
50
  */
56
51
  @serializable()
57
52
  membership: number[] = [0];
58
53
 
59
54
  /**
60
- * The layers that this collider will interact with. Used for filtering collision detection.
55
+ * The layers that the collider will interact with.
56
+ * @inheritdoc
61
57
  */
62
58
  @serializable()
63
59
  filter?: number[];
@@ -84,91 +80,62 @@
84
80
  this.context.physics.engine?.removeBody(this);
85
81
  }
86
82
 
87
- /**
88
- * Returns the underlying physics body from the physics engine.
89
- * Only available if the component is enabled and active in the scene.
90
- */
83
+ /** Returns the underlying physics body from the physics engine (if any) - the component must be enabled and active in the scene */
91
84
  get body() {
92
85
  return this.context.physics.engine?.getBody(this);
93
86
  }
94
87
 
95
88
  /**
96
- * Updates the collider's properties in the physics engine.
97
- * Use this when you've changed collider properties and need to sync with the physics engine.
89
+ * Apply the collider properties to the physics engine.
98
90
  */
99
91
  updateProperties = () => {
100
92
  this.context.physics.engine?.updateProperties(this);
101
93
  }
102
94
 
103
- /**
104
- * Updates the physics material in the physics engine.
105
- * Call this after changing the sharedMaterial property.
106
- */
95
+ /** Requests an update of the physics material in the physics engine */
107
96
  updatePhysicsMaterial() {
108
97
  this.context.physics.engine?.updatePhysicsMaterial(this);
98
+
109
99
  }
110
100
  }
111
101
 
112
102
  /**
113
- * SphereCollider represents a sphere-shaped collision volume.
114
- * Useful for objects that are roughly spherical in shape or need a simple collision boundary.
103
+ * SphereCollider is a collider that represents a sphere shape.
115
104
  * @category Physics
116
105
  * @group Components
117
106
  */
118
107
  export class SphereCollider extends Collider implements ISphereCollider {
119
108
 
120
- /**
121
- * The radius of the sphere collider.
122
- */
123
109
  @validate()
124
110
  @serializable()
125
111
  radius: number = .5;
126
112
 
127
- /**
128
- * The center position of the sphere collider relative to the transform's position.
129
- */
130
113
  @serializable(Vector3)
131
114
  center: Vector3 = new Vector3(0, 0, 0);
132
115
 
133
- /**
134
- * Registers the sphere collider with the physics engine and sets up scale change monitoring.
135
- */
136
116
  onEnable() {
137
117
  super.onEnable();
138
118
  this.context.physics.engine?.addSphereCollider(this);
139
119
  watchWrite(this.gameObject.scale, this.updateProperties);
140
120
  }
141
121
 
142
- /**
143
- * Removes scale change monitoring when the collider is disabled.
144
- */
145
122
  onDisable(): void {
146
123
  super.onDisable();
147
124
  unwatchWrite(this.gameObject.scale, this.updateProperties);
148
125
  }
149
126
 
150
- /**
151
- * Updates collider properties when validated in the editor or inspector.
152
- */
153
127
  onValidate(): void {
154
128
  this.updateProperties();
155
129
  }
156
130
  }
157
131
 
158
132
  /**
159
- * BoxCollider represents a box-shaped collision volume.
160
- * Ideal for rectangular objects or objects that need a simple cuboid collision boundary.
133
+ * BoxCollider is a collider that represents a box shape.
161
134
  * @category Physics
162
135
  * @group Components
163
136
  */
164
137
  export class BoxCollider extends Collider implements IBoxCollider {
165
138
 
166
- /**
167
- * Creates and adds a BoxCollider to the given object.
168
- * @param obj The object to add the collider to
169
- * @param opts Configuration options for the collider and optional rigidbody
170
- * @returns The newly created BoxCollider
171
- */
172
139
  static add(obj: Mesh | Object3D, opts?: { rigidbody: boolean, debug?: boolean }) {
173
140
  const collider = addComponent(obj, BoxCollider);
174
141
  collider.autoFit();
@@ -179,51 +146,31 @@
179
146
  return collider;
180
147
  }
181
148
 
182
- /**
183
- * The size of the box collider along each axis.
184
- */
185
149
  @validate()
186
150
  @serializable(Vector3)
187
151
  size: Vector3 = new Vector3(1, 1, 1);
188
152
 
189
- /**
190
- * The center position of the box collider relative to the transform's position.
191
- */
192
153
  @serializable(Vector3)
193
154
  center: Vector3 = new Vector3(0, 0, 0);
194
155
 
195
- /**
196
- * Registers the box collider with the physics engine and sets up scale change monitoring.
197
- * @internal
198
- */
156
+ /** @internal */
199
157
  onEnable() {
200
158
  super.onEnable();
201
159
  this.context.physics.engine?.addBoxCollider(this, this.size);
202
160
  watchWrite(this.gameObject.scale, this.updateProperties);
203
161
  }
204
162
 
205
- /**
206
- * Removes scale change monitoring when the collider is disabled.
207
- * @internal
208
- */
163
+ /** @internal */
209
164
  onDisable(): void {
210
165
  super.onDisable();
211
166
  unwatchWrite(this.gameObject.scale, this.updateProperties);
212
167
  }
213
168
 
214
- /**
215
- * Updates collider properties when validated in the editor or inspector.
216
- * @internal
217
- */
169
+ /** @internal */
218
170
  onValidate(): void {
219
171
  this.updateProperties();
220
172
  }
221
173
 
222
- /**
223
- * Automatically fits the collider to the geometry of the object.
224
- * Sets the size and center based on the object's bounding box.
225
- * @param opts Options object with a debug flag to visualize the bounding box
226
- */
227
174
  autoFit(opts?: { debug?: boolean }) {
228
175
  const obj = this.gameObject;
229
176
 
@@ -258,31 +205,24 @@
258
205
  }
259
206
 
260
207
  /**
261
- * MeshCollider creates a collision shape from a mesh geometry.
262
- * Allows for complex collision shapes that match the exact geometry of an object.
208
+ * MeshCollider is a collider that represents a mesh shape.
209
+ * The mesh collider can be used to create a collider from a mesh.
263
210
  * @category Physics
264
211
  * @group Components
265
212
  */
266
213
  export class MeshCollider extends Collider {
267
214
 
268
215
  /**
269
- * The mesh that is used to create the collision shape.
270
- * If not set, the collider will try to use the mesh of the object it's attached to.
216
+ * The mesh that is used for the collider.
271
217
  */
272
218
  @serializable(Mesh)
273
219
  sharedMesh?: Mesh;
274
220
 
275
- /**
276
- * When `true` the collider is treated as a solid object without holes.
277
- * Set to `false` if you want this mesh collider to be able to contain other objects.
278
- */
221
+ /** When `true` the collider won't have holes or entrances.
222
+ * If you wan't this mesh collider to be able to *contain* other objects this should be set to `false` */
279
223
  @serializable()
280
224
  convex: boolean = false;
281
225
 
282
- /**
283
- * Creates and registers the mesh collider with the physics engine.
284
- * Handles both individual meshes and mesh groups.
285
- */
286
226
  onEnable() {
287
227
  super.onEnable();
288
228
  if (!this.context.physics.engine) return;
@@ -332,44 +272,29 @@
332
272
  });
333
273
  }
334
274
  else {
335
- if (isDevEnvironment() || getParam("showcolliders")) {
336
- console.warn(`[MeshCollider] A MeshCollider mesh is assigned to an unknown object on \"${this.gameObject.name}\", but it's neither a Mesh nor a Group. Please double check that you attached the collider component to the right object and report a bug otherwise!`, this);
337
- }
275
+ console.warn("A MeshCollider mesh is assigned, but it's neither a Mesh nor a Group. Please report a bug!", this, this.sharedMesh);
338
276
  }
339
277
  }
340
278
  }
341
279
  }
342
280
 
343
281
  /**
344
- * CapsuleCollider represents a capsule-shaped collision volume (cylinder with hemispherical ends).
345
- * Ideal for character controllers and objects that need a rounded collision shape.
282
+ * CapsuleCollider is a collider that represents a capsule shape.
346
283
  * @category Physics
347
284
  * @group Components
348
285
  */
349
286
  export class CapsuleCollider extends Collider {
350
- /**
351
- * The center position of the capsule collider relative to the transform's position.
352
- */
353
287
  @serializable(Vector3)
354
288
  center: Vector3 = new Vector3(0, 0, 0);
355
289
 
356
- /**
357
- * The radius of the capsule's cylindrical body and hemispherical ends.
358
- */
359
290
  @serializable()
360
291
  radius: number = .5;
361
-
362
- /**
363
- * The total height of the capsule including both hemispherical ends.
364
- */
365
292
  @serializable()
366
293
  height: number = 2;
367
294
 
368
- /**
369
- * Registers the capsule collider with the physics engine.
370
- */
371
295
  onEnable() {
372
296
  super.onEnable();
373
297
  this.context.physics.engine?.addCapsuleCollider(this, this.height, this.radius);
374
298
  }
299
+
375
300
  }
src/engine-components/Component.ts CHANGED
@@ -23,167 +23,57 @@
23
23
  // }
24
24
 
25
25
  /**
26
- * Base class for objects in Needle Engine. Extends {@link Object3D} from three.js.
27
- * GameObjects can have components attached to them, which can be used to add functionality to the object.
28
- * They manage their components and provide methods to add, remove and get components.
29
- *
30
- * All {@link Object3D} types loaded in Needle Engine have methods like {@link addComponent}.
31
- * These methods are available directly on the GameObject instance:
26
+ * All {@type Object3D} types that are loaded in Needle Engine do automatically receive the GameObject extensions like `addComponent` etc.
27
+ * Many of the GameObject methods can be imported directly via `@needle-tools/engine` as well:
32
28
  * ```typescript
33
- * target.addComponent(MyComponent);
29
+ * import { addComponent } from "@needle-tools/engine";
34
30
  * ```
35
- *
36
- * And can be called statically on the GameObject class as well:
37
- * ```typescript
38
- * GameObject.setActive(target, true);
39
- * ```
40
31
  */
41
32
  export abstract class GameObject extends Object3D implements Object3D, IGameObject {
42
33
 
43
- /**
44
- * Indicates if the GameObject is currently active. Inactive GameObjects will not be rendered or updated.
45
- * When the activeSelf state changes, components will receive {@link Component.onEnable} or {@link Component.onDisable} callbacks.
46
- */
34
+ // these are implemented via threejs object extensions
47
35
  abstract activeSelf: boolean;
48
36
 
49
- /** @deprecated Use {@link addComponent} instead */
37
+ /** @deprecated use `addComponent` */
50
38
  // eslint-disable-next-line deprecation/deprecation
51
39
  abstract addNewComponent<T extends IComponent>(type: ConstructorConcrete<T>, init?: ComponentInit<T>): T;
52
-
53
- /**
54
- * Creates a new component on this gameObject or adds an existing component instance
55
- * @param comp Component type constructor or existing component instance
56
- * @param init Optional initialization values for the component
57
- * @returns The newly created or added component
58
- */
40
+ /** creates a new component on this gameObject */
59
41
  abstract addComponent<T extends IComponent>(comp: T | ConstructorConcrete<T>, init?: ComponentInit<T>): T;
60
-
61
- /**
62
- * Removes a component from this GameObject
63
- * @param comp Component instance to remove
64
- * @returns The removed component
65
- */
66
42
  abstract removeComponent<T extends IComponent>(comp: T): T;
67
-
68
- /**
69
- * Gets an existing component of the specified type or adds a new one if it doesn't exist
70
- * @param typeName Constructor of the component type to get or add
71
- * @returns The existing or newly added component
72
- */
73
43
  abstract getOrAddComponent<T>(typeName: ConstructorConcrete<T> | null): T;
74
-
75
- /**
76
- * Gets a component of the specified type attached to this GameObject
77
- * @param type Constructor of the component type to get
78
- * @returns The component if found, otherwise null
79
- */
80
44
  abstract getComponent<T>(type: Constructor<T>): T | null;
81
-
82
- /**
83
- * Gets all components of the specified type attached to this GameObject
84
- * @param type Constructor of the component type to get
85
- * @param arr Optional array to populate with the components
86
- * @returns Array of components
87
- */
88
45
  abstract getComponents<T>(type: Constructor<T>, arr?: T[]): Array<T>;
89
-
90
- /**
91
- * Gets a component of the specified type in this GameObject's children hierarchy
92
- * @param type Constructor of the component type to get
93
- * @returns The first matching component if found, otherwise null
94
- */
95
46
  abstract getComponentInChildren<T>(type: Constructor<T>): T | null;
96
-
97
- /**
98
- * Gets all components of the specified type in this GameObject's children hierarchy
99
- * @param type Constructor of the component type to get
100
- * @param arr Optional array to populate with the components
101
- * @returns Array of components
102
- */
103
47
  abstract getComponentsInChildren<T>(type: Constructor<T>, arr?: T[]): Array<T>;
104
-
105
- /**
106
- * Gets a component of the specified type in this GameObject's parent hierarchy
107
- * @param type Constructor of the component type to get
108
- * @returns The first matching component if found, otherwise null
109
- */
110
48
  abstract getComponentInParent<T>(type: Constructor<T>): T | null;
111
-
112
- /**
113
- * Gets all components of the specified type in this GameObject's parent hierarchy
114
- * @param type Constructor of the component type to get
115
- * @param arr Optional array to populate with the components
116
- * @returns Array of components
117
- */
118
49
  abstract getComponentsInParent<T>(type: Constructor<T>, arr?: T[]): Array<T>;
119
50
 
120
51
 
121
- /**
122
- * The position of this GameObject in world space
123
- */
124
52
  abstract get worldPosition(): Vector3
125
53
  abstract set worldPosition(val: Vector3);
126
-
127
- /**
128
- * The rotation of this GameObject in world space as a quaternion
129
- */
130
54
  abstract set worldQuaternion(val: Quaternion);
131
55
  abstract get worldQuaternion(): Quaternion;
132
-
133
- /**
134
- * The rotation of this GameObject in world space in euler angles (degrees)
135
- */
136
56
  abstract set worldRotation(val: Vector3);
137
57
  abstract get worldRotation(): Vector3;
138
-
139
- /**
140
- * The scale of this GameObject in world space
141
- */
142
58
  abstract set worldScale(val: Vector3);
143
59
  abstract get worldScale(): Vector3;
144
60
 
145
- /**
146
- * The forward direction vector of this GameObject in world space
147
- */
148
61
  abstract get worldForward(): Vector3;
149
-
150
- /**
151
- * The right direction vector of this GameObject in world space
152
- */
153
62
  abstract get worldRight(): Vector3;
154
-
155
- /**
156
- * The up direction vector of this GameObject in world space
157
- */
158
63
  abstract get worldUp(): Vector3;
159
64
 
160
- /**
161
- * Unique identifier for this GameObject
162
- */
163
65
  guid: string | undefined;
164
66
 
165
- /**
166
- * Destroys this GameObject and all its components.
167
- * Internally, this is added to the three.js {@link Object3D} prototype.
168
- */
67
+ // Added to the threejs Object3D prototype
169
68
  abstract destroy();
170
69
 
171
70
 
172
- /**
173
- * Checks if a GameObject has been destroyed
174
- * @param go The GameObject to check
175
- * @returns True if the GameObject has been destroyed
176
- */
71
+
72
+
177
73
  public static isDestroyed(go: Object3D): boolean {
178
74
  return isDestroyed(go);
179
75
  }
180
76
 
181
- /**
182
- * Sets the active state of a GameObject
183
- * @param go The GameObject to modify
184
- * @param active Whether the GameObject should be active
185
- * @param processStart Whether to process the start callbacks if being activated
186
- */
187
77
  public static setActive(go: Object3D, active: boolean, processStart: boolean = true) {
188
78
  if (!go) return;
189
79
  setActive(go, active);
@@ -194,68 +84,47 @@
194
84
  main.processStart(Context.Current, go);
195
85
  }
196
86
 
197
- /**
198
- * Checks if the GameObject itself is active (same as go.visible)
199
- * @param go The GameObject to check
200
- * @returns True if the GameObject is active
201
- */
87
+ /** If the object is active (same as go.visible) */
202
88
  public static isActiveSelf(go: Object3D): boolean {
203
89
  return isActiveSelf(go);
204
90
  }
205
91
 
206
- /**
207
- * Checks if the GameObject is active in the hierarchy (e.g. if any parent is invisible or not in the scene it will be false)
208
- * @param go The GameObject to check
209
- * @returns True if the GameObject is active in the hierarchy
210
- */
92
+ /** If the object is active in the hierarchy (e.g. if any parent is invisible or not in the scene it will be false)
93
+ * @param go object to check
94
+ */
211
95
  public static isActiveInHierarchy(go: Object3D): boolean {
212
96
  return isActiveInHierarchy(go);
213
97
  }
214
98
 
215
- /**
216
- * Marks a GameObject to be rendered using instancing
217
- * @param go The GameObject to mark
218
- * @param instanced Whether the GameObject should use instanced rendering
219
- */
220
99
  public static markAsInstancedRendered(go: Object3D, instanced: boolean) {
221
100
  markAsInstancedRendered(go, instanced);
222
101
  }
223
102
 
224
- /**
225
- * Checks if a GameObject is using instanced rendering
226
- * @param instance The GameObject to check
227
- * @returns True if the GameObject is using instanced rendering
228
- */
229
103
  public static isUsingInstancing(instance: Object3D): boolean { return isUsingInstancing(instance); }
230
104
 
231
- /**
232
- * Executes a callback for all components of the provided type on the provided object and its children
233
- * @param instance Object to run the method on
234
- * @param cb Callback to run on each component, "return undefined;" to continue and "return <anything>;" to break the loop
235
- * @param recursive If true, the method will be run on all children as well
236
- * @returns The last return value of the callback
105
+ /** Run a callback for all components of the provided type on the provided object and its children (if recursive is true)
106
+ * @param instance object to run the method on
107
+ * @param cb callback to run on each component, "return undefined;" to continue and "return <anything>;" to break the loop
108
+ * @param recursive if true, the method will be run on all children as well
109
+ * @returns the last return value of the callback
237
110
  */
238
111
  public static foreachComponent(instance: Object3D, cb: (comp: Component) => any, recursive: boolean = true): any {
239
112
  return foreachComponent(instance, cb as (comp: IComponent) => any, recursive);
240
113
  }
241
114
 
242
- /**
243
- * Creates a new instance of the provided object that will be replicated to all connected clients
244
- * @param instance Object to instantiate
245
- * @param opts Options for the instantiation
246
- * @returns The newly created instance or null if creation failed
115
+ /** Creates a new instance of the provided object. The new instance will be created on all connected clients
116
+ * @param instance object to instantiate
117
+ * @param opts options for the instantiation
247
118
  */
248
119
  public static instantiateSynced(instance: GameObject | Object3D | null, opts: SyncInstantiateOptions): GameObject | null {
249
120
  if (!instance) return null;
250
121
  return syncInstantiate(instance, opts) as GameObject | null;
251
122
  }
252
123
 
253
- /**
254
- * Creates a new instance of the provided object (like cloning it including all components and children)
255
- * @param instance Object to instantiate
256
- * @param opts Options for the instantiation (e.g. with what parent, position, etc.)
257
- * @returns The newly created instance
258
- */
124
+ /** Creates a new instance of the provided object (like cloning it including all components and children)
125
+ * @param instance object to instantiate
126
+ * @param opts options for the instantiation (e.g. with what parent, position, etc.)
127
+ */
259
128
  public static instantiate(instance: AssetReference, opts?: IInstantiateOptions | null | undefined): Promise<Object3D | null>
260
129
  public static instantiate(instance: GameObject | Object3D, opts?: IInstantiateOptions | null | undefined): GameObject
261
130
  public static instantiate(instance: AssetReference | GameObject | Object3D, opts: IInstantiateOptions | null | undefined = null): GameObject | Promise<Object3D | null> {
@@ -265,12 +134,9 @@
265
134
  return instantiate(instance as GameObject | Object3D, opts) as GameObject;
266
135
  }
267
136
 
268
- /**
269
- * Destroys an object on all connected clients (if in a networked session)
270
- * @param instance Object to destroy
271
- * @param context Optional context to use
272
- * @param recursive If true, all children will be destroyed as well
273
- */
137
+ /** Destroys a object on all connected clients (if you are in a networked session)
138
+ * @param instance object to destroy
139
+ */
274
140
  public static destroySynced(instance: Object3D | Component, context?: Context, recursive: boolean = true) {
275
141
  if (!instance) return;
276
142
  const go = instance as GameObject;
@@ -278,20 +144,16 @@
278
144
  syncDestroy(go as any, context.connection, recursive);
279
145
  }
280
146
 
281
- /**
282
- * Destroys an object
283
- * @param instance Object to destroy
284
- * @param recursive If true, all children will be destroyed as well. Default: true
147
+ /** Destroys a object
148
+ * @param instance object to destroy
149
+ * @param recursive if true, all children will be destroyed as well. true by default
285
150
  */
286
151
  public static destroy(instance: Object3D | Component, recursive: boolean = true) {
287
152
  return destroy(instance, recursive);
288
153
  }
289
154
 
290
155
  /**
291
- * Adds an object to parent and ensures all components are properly registered
292
- * @param instance Object to add
293
- * @param parent Parent to add the object to
294
- * @param context Optional context to use
156
+ * Add an object to parent and also ensure all components are being registered
295
157
  */
296
158
  public static add(instance: Object3D | null | undefined, parent: Object3D, context?: Context) {
297
159
  if (!instance || !parent) return;
@@ -321,7 +183,6 @@
321
183
 
322
184
  /**
323
185
  * Removes the object from its parent and deactivates all of its components
324
- * @param instance Object to remove
325
186
  */
326
187
  public static remove(instance: Object3D | null | undefined) {
327
188
  if (!instance) return;
@@ -333,22 +194,14 @@
333
194
  }, true);
334
195
  }
335
196
 
336
- /**
337
- * Invokes a method on all components including children (if a method with that name exists)
338
- * @param go GameObject to invoke the method on
339
- * @param functionName Name of the method to invoke
340
- * @param args Arguments to pass to the method
341
- */
197
+ /** Invokes a method on all components including children (if a method with that name exists) */
342
198
  public static invokeOnChildren(go: Object3D | null | undefined, functionName: string, ...args: any) {
343
199
  this.invoke(go, functionName, true, args);
344
200
  }
345
201
 
346
- /**
347
- * Invokes a method on all components that have a method matching the provided name
348
- * @param go GameObject to invoke the method on
349
- * @param functionName Name of the method to invoke
350
- * @param children Whether to invoke on children as well
351
- * @param args Arguments to pass to the method
202
+ /** Invokes a method on all components that have a method matching the provided name
203
+ * @param go object to invoke the method on all components
204
+ * @param functionName name of the method to invoke
352
205
  */
353
206
  public static invoke(go: Object3D | null | undefined, functionName: string, children: boolean = false, ...args: any) {
354
207
  if (!go) return;
@@ -367,53 +220,38 @@
367
220
  }
368
221
 
369
222
  /**
370
- * Adds a new component (or moves an existing component) to the provided object
371
- * @param go Object to add the component to
372
- * @param instanceOrType If an instance is provided it will be moved to the new object, if a type is provided a new instance will be created
373
- * @param init Optional init object to initialize the component with
374
- * @param opts Optional options for adding the component
375
- * @returns The added or moved component
223
+ * Add a new component (or move an existing component) to the provided object
224
+ * @param go object to add the component to
225
+ * @param instanceOrType if an instance is provided it will be moved to the new object, if a type is provided a new instance will be created and moved to the new object
226
+ * @param init optional init object to initialize the component with
227
+ * @param callAwake if true, the component will be added and awake will be called immediately
376
228
  */
377
229
  public static addComponent<T extends IComponent>(go: IGameObject | Object3D, instanceOrType: T | ConstructorConcrete<T>, init?: ComponentInit<T>, opts?: { callAwake: boolean }): T {
378
230
  return addComponent(go, instanceOrType, init, opts);
379
231
  }
380
232
 
381
233
  /**
382
- * Moves a component to a new object
383
- * @param go GameObject to move the component to
384
- * @param instance Component to move
385
- * @returns The moved component
234
+ * Moves a component to a new object
235
+ * @param go component to move the component to
236
+ * @param instance component to move to the GO
386
237
  */
387
238
  public static moveComponent<T extends IComponent>(go: IGameObject | Object3D, instance: T | ConstructorConcrete<T>): T {
388
239
  return addComponent(go, instance);
389
240
  }
390
241
 
391
- /**
392
- * Removes a component from its object
393
- * @param instance Component to remove
394
- * @returns The removed component
242
+ /** Removes a component from its object
243
+ * @param instance component to remove
395
244
  */
396
245
  public static removeComponent<T extends IComponent>(instance: T): T {
397
246
  removeComponent(instance.gameObject, instance as any);
398
247
  return instance;
399
248
  }
400
249
 
401
- /**
402
- * Gets or adds a component of the specified type
403
- * @param go GameObject to get or add the component to
404
- * @param typeName Constructor of the component type
405
- * @returns The existing or newly added component
406
- */
407
250
  public static getOrAddComponent<T extends IComponent>(go: IGameObject | Object3D, typeName: ConstructorConcrete<T>): T {
408
251
  return getOrAddComponent<any>(go, typeName);
409
252
  }
410
253
 
411
- /**
412
- * Gets a component on the provided object
413
- * @param go GameObject to get the component from
414
- * @param typeName Constructor of the component type
415
- * @returns The component if found, otherwise null
416
- */
254
+ /** Gets a component on the provided object */
417
255
  public static getComponent<T extends IComponent>(go: IGameObject | Object3D | null, typeName: Constructor<T> | null): T | null {
418
256
  if (go === null) return null;
419
257
  // if names are minified we could also use the type store and work with strings everywhere
@@ -423,99 +261,42 @@
423
261
  return getComponent(go, typeName as any);
424
262
  }
425
263
 
426
- /**
427
- * Gets all components of the specified type on the provided object
428
- * @param go GameObject to get the components from
429
- * @param typeName Constructor of the component type
430
- * @param arr Optional array to populate with the components
431
- * @returns Array of components
432
- */
433
264
  public static getComponents<T extends IComponent>(go: IGameObject | Object3D | null, typeName: Constructor<T>, arr: T[] | null = null): T[] {
434
265
  if (go === null) return arr ?? [];
435
266
  return getComponents(go, typeName, arr);
436
267
  }
437
268
 
438
- /**
439
- * Finds an object or component by its unique identifier
440
- * @param guid Unique identifier to search for
441
- * @param hierarchy Root object to search in
442
- * @returns The found GameObject or Component, or null/undefined if not found
443
- */
444
269
  public static findByGuid(guid: string, hierarchy: Object3D): GameObject | Component | null | undefined {
445
270
  const res = findByGuid(guid, hierarchy);
446
271
  return res as GameObject | Component | null | undefined;
447
272
  }
448
273
 
449
- /**
450
- * Finds the first object of the specified component type in the scene
451
- * @param typeName Constructor of the component type
452
- * @param context Context or root object to search in
453
- * @param includeInactive Whether to include inactive objects in the search
454
- * @returns The first matching component if found, otherwise null
455
- */
456
274
  public static findObjectOfType<T extends IComponent>(typeName: Constructor<T>, context?: Context | Object3D, includeInactive: boolean = true): T | null {
457
275
  return findObjectOfType(typeName, context ?? Context.Current, includeInactive);
458
276
  }
459
277
 
460
- /**
461
- * Finds all objects of the specified component type in the scene
462
- * @param typeName Constructor of the component type
463
- * @param context Context or root object to search in
464
- * @returns Array of matching components
465
- */
466
278
  public static findObjectsOfType<T extends IComponent>(typeName: Constructor<T>, context?: Context | Object3D): Array<T> {
467
279
  const arr = [];
468
280
  findObjectsOfType(typeName, arr, context);
469
281
  return arr;
470
282
  }
471
283
 
472
- /**
473
- * Gets a component of the specified type in the gameObject's children hierarchy
474
- * @param go GameObject to search in
475
- * @param typeName Constructor of the component type
476
- * @returns The first matching component if found, otherwise null
477
- */
478
284
  public static getComponentInChildren<T extends IComponent>(go: IGameObject | Object3D, typeName: Constructor<T>): T | null {
479
285
  return getComponentInChildren(go, typeName);
480
286
  }
481
287
 
482
- /**
483
- * Gets all components of the specified type in the gameObject's children hierarchy
484
- * @param go GameObject to search in
485
- * @param typeName Constructor of the component type
486
- * @param arr Optional array to populate with the components
487
- * @returns Array of components
488
- */
489
288
  public static getComponentsInChildren<T extends IComponent>(go: IGameObject | Object3D, typeName: Constructor<T>, arr: T[] | null = null): Array<T> {
490
289
  return getComponentsInChildren<T>(go, typeName, arr ?? undefined) as T[]
491
290
  }
492
291
 
493
- /**
494
- * Gets a component of the specified type in the gameObject's parent hierarchy
495
- * @param go GameObject to search in
496
- * @param typeName Constructor of the component type
497
- * @returns The first matching component if found, otherwise null
498
- */
499
292
  public static getComponentInParent<T extends IComponent>(go: IGameObject | Object3D, typeName: Constructor<T>): T | null {
500
293
  return getComponentInParent(go, typeName);
501
294
  }
502
295
 
503
- /**
504
- * Gets all components of the specified type in the gameObject's parent hierarchy
505
- * @param go GameObject to search in
506
- * @param typeName Constructor of the component type
507
- * @param arr Optional array to populate with the components
508
- * @returns Array of components
509
- */
510
296
  public static getComponentsInParent<T extends IComponent>(go: IGameObject | Object3D, typeName: Constructor<T>, arr: Array<T> | null = null): Array<T> {
511
297
  return getComponentsInParent(go, typeName, arr);
512
298
  }
513
299
 
514
- /**
515
- * Gets all components on the gameObject
516
- * @param go GameObject to get components from
517
- * @returns Array of all components
518
- */
519
300
  public static getAllComponents(go: IGameObject | Object3D): Component[] {
520
301
  const componentsList = go.userData?.components;
521
302
  if (!componentsList) return [];
@@ -523,11 +304,6 @@
523
304
  return newList;
524
305
  }
525
306
 
526
- /**
527
- * Iterates through all components on the gameObject
528
- * @param go GameObject to iterate components on
529
- * @returns Generator yielding each component
530
- */
531
307
  public static *iterateComponents(go: IGameObject | Object3D) {
532
308
  const list = go?.userData?.components;
533
309
  if (list && Array.isArray(list)) {
@@ -570,43 +346,27 @@
570
346
  Partial<INeedleXRSessionEventReceiver>,
571
347
  Partial<IPointerEventHandler>
572
348
  {
573
- /**
574
- * Indicates whether this object is a component
575
- * @internal
576
- */
349
+ /** @internal */
577
350
  get isComponent(): boolean { return true; }
578
351
 
579
352
  private __context: Context | undefined;
580
-
581
- /**
582
- * The context this component belongs to, providing access to the runtime environment
583
- * including physics, timing utilities, camera, and scene
584
- */
353
+ /** Use the context to get access to many Needle Engine features and use physics, timing, access the camera or scene */
585
354
  get context(): Context {
586
355
  return this.__context ?? Context.Current;
587
356
  }
588
357
  set context(context: Context) {
589
358
  this.__context = context;
590
359
  }
591
-
592
- /**
593
- * Shorthand accessor for the current scene from the context
594
- * @returns The scene this component belongs to
595
- */
360
+ /** shorthand for `this.context.scene`
361
+ * @returns the scene of the context */
596
362
  get scene(): Scene { return this.context.scene; }
597
363
 
598
- /**
599
- * The layer value of the GameObject this component is attached to
600
- * Used for visibility and physics filtering
601
- */
364
+ /** @returns the layer of the gameObject this component is attached to */
602
365
  get layer(): number {
603
366
  return this.gameObject?.userData?.layer;
604
367
  }
605
368
 
606
- /**
607
- * The name of the GameObject this component is attached to
608
- * Used for debugging and finding objects
609
- */
369
+ /** @returns the name of the gameObject this component is attached to */
610
370
  get name(): string {
611
371
  if (this.gameObject?.name) {
612
372
  return this.gameObject.name;
@@ -624,11 +384,7 @@
624
384
  this.__name = str;
625
385
  }
626
386
  }
627
-
628
- /**
629
- * The tag of the GameObject this component is attached to
630
- * Used for categorizing objects and efficient lookup
631
- */
387
+ /** @returns the tag of the gameObject this component is attached to */
632
388
  get tag(): string {
633
389
  return this.gameObject?.userData.tag;
634
390
  }
@@ -638,11 +394,7 @@
638
394
  this.gameObject.userData.tag = str;
639
395
  }
640
396
  }
641
-
642
- /**
643
- * Indicates whether the GameObject is marked as static
644
- * Static objects typically don't move and can be optimized by the engine
645
- */
397
+ /** Is the gameObject marked as static */
646
398
  get static() {
647
399
  return this.gameObject?.userData.static;
648
400
  }
@@ -656,11 +408,7 @@
656
408
  // return this.gameObject?.hideFlags;
657
409
  // }
658
410
 
659
- /**
660
- * Checks if this component is currently active (enabled and part of an active GameObject hierarchy)
661
- * Components that are inactive won't receive lifecycle method calls
662
- * @returns True if the component is enabled and all parent GameObjects are active
663
- */
411
+ /** @returns true if the object is enabled and active in the hierarchy */
664
412
  get activeAndEnabled(): boolean {
665
413
  if (this.destroyed) return false;
666
414
  if (this.__isEnabled === false) return false;
@@ -678,7 +426,6 @@
678
426
  private get __isActive(): boolean {
679
427
  return this.gameObject.visible;
680
428
  }
681
-
682
429
  private get __isActiveInHierarchy(): boolean {
683
430
  if (!this.gameObject) return false;
684
431
  const res = this.gameObject[activeInHierarchyFieldName];
@@ -691,286 +438,139 @@
691
438
  this.gameObject[activeInHierarchyFieldName] = val;
692
439
  }
693
440
 
694
- /**
695
- * Reference to the GameObject this component is attached to
696
- * This is a three.js Object3D with additional GameObject functionality
697
- */
441
+ /** the object this component is attached to. Note that this is a threejs Object3D with some additional features */
698
442
  gameObject!: GameObject;
699
-
700
- /**
701
- * Unique identifier for this component instance,
702
- * used for finding and tracking components
703
- */
443
+ /** the unique identifier for this component */
704
444
  guid: string = "invalid";
705
-
706
- /**
707
- * Identifier for the source asset that created this component.
708
- * For example, URL to the glTF file this component was loaded from
709
- */
445
+ /** holds the source identifier this object was created with/from (e.g. if it was part of a glTF file the sourceId holds the url to the glTF) */
710
446
  sourceId?: SourceIdentifier;
447
+ // transform: Object3D = nullObject;
711
448
 
712
- /**
713
- * Called when this component needs to remap guids after an instantiate operation.
714
- * @param guidsMap Mapping from old guids to newly generated guids
715
- */
449
+ /** called on a component with a map of old to new guids (e.g. when instantiate generated new guids and e.g. timeline track bindings needs to remape them) */
716
450
  resolveGuids?(guidsMap: GuidsMap): void;
717
451
 
718
- /**
719
- * Called once when the component becomes active for the first time.
720
- * This is the first lifecycle callback to be invoked
721
- */
452
+ /** called once when the component becomes active for the first time (once per component)
453
+ * This is the first callback to be called */
722
454
  awake() { }
723
-
724
- /**
725
- * Called every time the component becomes enabled or active in the hierarchy.
726
- * Invoked after {@link awake} and before {@link start}.
727
- */
455
+ /** called every time when the component gets enabled (this is invoked after awake and before start)
456
+ * or when it becomes active in the hierarchy (e.g. if a parent object or this.gameObject gets set to visible)
457
+ */
728
458
  onEnable() { }
729
-
730
- /**
731
- * Called every time the component becomes disabled or inactive in the hierarchy.
732
- * Invoked when the component or any parent GameObject becomes invisible
733
- */
459
+ /** called every time the component gets disabled or if a parent object (or this.gameObject) gets set to invisible */
734
460
  onDisable() { }
735
-
736
- /**
737
- * Called when the component is destroyed.
738
- * Use for cleanup operations like removing event listeners
739
- */
461
+ /** Called when the component gets destroyed */
740
462
  onDestroy() {
741
463
  this.__destroyed = true;
742
464
  }
743
-
744
- /**
745
- * Called when a field decorated with @validate() is modified.
746
- * @param prop The name of the field that was changed
465
+ /** called when you decorate fields with the @validate() decorator
466
+ * @param prop the name of the field that was changed
747
467
  */
748
468
  onValidate?(prop?: string): void;
749
469
 
750
- /**
751
- * Called when the context's pause state changes.
752
- * @param isPaused Whether the context is currently paused
753
- * @param wasPaused The previous pause state
754
- */
470
+ /** Called for all scripts when the context gets paused or unpaused */
755
471
  onPausedChanged?(isPaused: boolean, wasPaused: boolean): void;
756
472
 
757
- /**
758
- * Called once at the beginning of the first frame after the component is enabled.
759
- * Use for initialization that requires other components to be awake.
760
- */
473
+ /** called at the beginning of a frame (once per component) */
761
474
  start?(): void;
762
-
763
- /**
764
- * Called at the beginning of each frame before regular updates.
765
- * Use for logic that needs to run before standard update callbacks.
766
- */
475
+ /** first callback in a frame (called every frame when implemented) */
767
476
  earlyUpdate?(): void;
768
-
769
- /**
770
- * Called once per frame during the main update loop.
771
- * The primary location for frame-based game logic.
772
- */
477
+ /** regular callback in a frame (called every frame when implemented) */
773
478
  update?(): void;
774
-
775
- /**
776
- * Called after all update functions have been called.
777
- * Use for calculations that depend on other components being updated first.
778
- */
479
+ /** late callback in a frame (called every frame when implemented) */
779
480
  lateUpdate?(): void;
780
-
781
- /**
782
- * Called immediately before the scene is rendered.
783
- * @param frame Current XRFrame if in an XR session, null otherwise
784
- */
481
+ /** called before the scene gets rendered in the main update loop */
785
482
  onBeforeRender?(frame: XRFrame | null): void;
786
-
787
- /**
788
- * Called after the scene has been rendered.
789
- * Use for post-processing or UI updates that should happen after rendering
790
- */
483
+ /** called after the scene was rendered */
791
484
  onAfterRender?(): void;
792
485
 
793
- /**
794
- * Called when this component's collider begins colliding with another collider.
795
- * @param col Information about the collision that occurred
796
- */
797
486
  onCollisionEnter?(col: Collision);
798
-
799
- /**
800
- * Called when this component's collider stops colliding with another collider.
801
- * @param col Information about the collision that ended
802
- */
803
487
  onCollisionExit?(col: Collision);
804
-
805
- /**
806
- * Called each frame while this component's collider is colliding with another collider
807
- * @param col Information about the ongoing collision
808
- */
809
488
  onCollisionStay?(col: Collision);
810
489
 
811
- /**
812
- * Called when this component's trigger collider is entered by another collider
813
- * @param col The collider that entered this trigger
814
- */
815
490
  onTriggerEnter?(col: ICollider);
816
-
817
- /**
818
- * Called each frame while another collider is inside this component's trigger collider
819
- * @param col The collider that is inside this trigger
820
- */
821
491
  onTriggerStay?(col: ICollider);
822
-
823
- /**
824
- * Called when another collider exits this component's trigger collider
825
- * @param col The collider that exited this trigger
826
- */
827
492
  onTriggerExit?(col: ICollider);
828
493
 
829
- /**
830
- * Determines if this component supports a specific XR mode
831
- * @param mode The XR session mode to check support for
832
- * @returns True if the component supports the specified mode
833
- */
494
+
495
+ /** Optional callback, you can implement this to only get callbacks for VR or AR sessions if necessary.
496
+ * @returns true if the mode is supported (if false the mode is not supported by this component and it will not receive XR callbacks for this mode)
497
+ */
834
498
  supportsXR?(mode: XRSessionMode): boolean;
835
-
836
- /**
837
- * Called before an XR session is requested
838
- * Use to modify session initialization parameters
839
- * @param mode The XR session mode being requested
840
- * @param args The session initialization parameters that can be modified
841
- */
499
+ /** Called before the XR session is requested. Use this callback if you want to modify the session init features */
842
500
  onBeforeXR?(mode: XRSessionMode, args: XRSessionInit): void;
843
-
844
- /**
845
- * Called when this component joins an XR session or becomes active in a running session
846
- * @param args Event data for the XR session
847
- */
501
+ /** Callback when this component joins a xr session (or becomes active in a running XR session) */
848
502
  onEnterXR?(args: NeedleXREventArgs): void;
849
-
850
- /**
851
- * Called each frame while this component is active in an XR session
852
- * @param args Event data for the current XR frame
853
- */
503
+ /** Callback when a xr session updates (while it is still active in XR session) */
854
504
  onUpdateXR?(args: NeedleXREventArgs): void;
855
-
856
- /**
857
- * Called when this component exits an XR session or becomes inactive during a session
858
- * @param args Event data for the XR session
859
- */
505
+ /** Callback when this component exists a xr session (or when it becomes inactive in a running XR session) */
860
506
  onLeaveXR?(args: NeedleXREventArgs): void;
861
-
862
- /**
863
- * Called when an XR controller is connected or when this component becomes active
864
- * in a session with existing controllers
865
- * @param args Event data for the controller that was added
866
- */
507
+ /** Callback when a controller is connected/added while in a XR session
508
+ * OR when the component joins a running XR session that has already connected controllers
509
+ * OR when the component becomes active during a running XR session that has already connected controllers */
867
510
  onXRControllerAdded?(args: NeedleXRControllerEventArgs): void;
868
-
869
- /**
870
- * Called when an XR controller is disconnected or when this component becomes inactive
871
- * during a session with controllers
872
- * @param args Event data for the controller that was removed
873
- */
511
+ /** callback when a controller is removed while in a XR session
512
+ * OR when the component becomes inactive during a running XR session
513
+ */
874
514
  onXRControllerRemoved?(args: NeedleXRControllerEventArgs): void;
875
515
 
876
- /**
877
- * Called when a pointer enters this component's GameObject
878
- * @param args Data about the pointer event
879
- */
516
+
517
+ /* IPointerEventReceiver */
518
+ /* @inheritdoc */
880
519
  onPointerEnter?(args: PointerEventData);
881
-
882
- /**
883
- * Called when a pointer moves while over this component's GameObject
884
- * @param args Data about the pointer event
885
- */
886
520
  onPointerMove?(args: PointerEventData);
887
-
888
- /**
889
- * Called when a pointer exits this component's GameObject
890
- * @param args Data about the pointer event
891
- */
892
521
  onPointerExit?(args: PointerEventData);
893
-
894
- /**
895
- * Called when a pointer button is pressed while over this component's GameObject
896
- * @param args Data about the pointer event
897
- */
898
522
  onPointerDown?(args: PointerEventData);
899
-
900
- /**
901
- * Called when a pointer button is released while over this component's GameObject
902
- * @param args Data about the pointer event
903
- */
904
523
  onPointerUp?(args: PointerEventData);
905
-
906
- /**
907
- * Called when a pointer completes a click interaction with this component's GameObject
908
- * @param args Data about the pointer event
909
- */
910
524
  onPointerClick?(args: PointerEventData);
911
525
 
912
- /**
913
- * Starts a coroutine that can yield to wait for events.
914
- * Coroutines allow for time-based sequencing of operations without blocking.
915
- * Coroutines are based on generator functions, a JavaScript language feature.
916
- *
917
- * @param routine Generator function to start
918
- * @param evt Event to register the coroutine for (default: FrameEvent.Update)
919
- * @returns The generator function that can be used to stop the coroutine
920
- * @example
921
- * Time-based sequencing of operations
526
+
527
+ /** starts a coroutine (javascript generator function)
528
+ * `yield` will wait for the next frame:
529
+ * - Use `yield WaitForSeconds(1)` to wait for 1 second.
530
+ * - Use `yield WaitForFrames(10)` to wait for 10 frames.
531
+ * - Use `yield new Promise(...)` to wait for a promise to resolve.
532
+ * @param routine generator function to start
533
+ * @param evt event to register the coroutine for (default: FrameEvent.Update). Note that all coroutine FrameEvent callbacks are invoked after the matching regular component callbacks. For example `FrameEvent.Update` will be called after regular component `update()` methods)
534
+ * @returns the generator function (use it to stop the coroutine with `stopCoroutine`)
535
+ * @example
922
536
  * ```ts
923
- * *myCoroutine() {
924
- * yield WaitForSeconds(1); // wait for 1 second
925
- * yield WaitForFrames(10); // wait for 10 frames
926
- * yield new Promise(resolve => setTimeout(resolve, 1000)); // wait for a promise to resolve
927
- * }
928
- * ```
929
- * @example
930
- * Coroutine that logs a message every 5 frames
931
- * ```ts
932
- * onEnable() {
933
- * this.startCoroutine(this.myCoroutine());
934
- * }
537
+ * onEnable() { this.startCoroutine(this.myCoroutine()); }
935
538
  * private *myCoroutine() {
936
- * while(this.activeAndEnabled) {
937
- * console.log("Hello World", this.context.time.frame);
938
- * // wait for 5 frames
939
- * for(let i = 0; i < 5; i++) yield;
940
- * }
539
+ * while(this.activeAndEnabled) {
540
+ * console.log("Hello World", this.context.time.frame);
541
+ * // wait for 5 frames
542
+ * for(let i = 0; i < 5; i++) yield;
543
+ * }
941
544
  * }
942
545
  * ```
943
546
  */
944
547
  startCoroutine(routine: Generator, evt: FrameEvent = FrameEvent.Update): Generator {
945
548
  return this.context.registerCoroutineUpdate(this, routine, evt);
946
549
  }
947
-
948
550
  /**
949
- * Stops a coroutine that was previously started with startCoroutine
950
- * @param routine The routine to be stopped
951
- * @param evt The frame event the routine was registered with
551
+ * Stop a coroutine that was previously started with `startCoroutine`
552
+ * @param routine the routine to be stopped
553
+ * @param evt the frame event to unregister the routine from (default: FrameEvent.Update)
952
554
  */
953
555
  stopCoroutine(routine: Generator, evt: FrameEvent = FrameEvent.Update): void {
954
556
  this.context.unregisterCoroutineUpdate(routine, evt);
955
557
  }
956
558
 
957
- /**
958
- * Checks if this component has been destroyed
959
- * @returns True if the component or its GameObject has been destroyed
960
- */
559
+ /** @returns true if this component was destroyed (`this.destroy()`) or the whole object this component was part of */
961
560
  public get destroyed(): boolean {
962
561
  return this.__destroyed;
963
562
  }
964
563
 
965
564
  /**
966
- * Destroys this component and removes it from its GameObject
967
- * After destruction, the component will no longer receive lifecycle callbacks
565
+ * Destroys this component (and removes it from the object)
968
566
  */
969
567
  public destroy() {
970
568
  if (this.__destroyed) return;
971
569
  this.__internalDestroy();
972
570
  }
973
571
 
572
+
573
+
974
574
  /** @internal */
975
575
  protected __didAwake: boolean = false;
976
576
 
@@ -989,6 +589,7 @@
989
589
  /** @internal */
990
590
  get __internalDidAwakeAndStart() { return this.__didAwake && this.__didStart; }
991
591
 
592
+
992
593
  /** @internal */
993
594
  constructor(init?: ComponentInit<Component>) {
994
595
  this.__didAwake = false;
@@ -1010,11 +611,6 @@
1010
611
  return this;
1011
612
  }
1012
613
 
1013
- /**
1014
- * Initializes component properties from an initialization object
1015
- * @param init Object with properties to copy to this component
1016
- * @internal
1017
- */
1018
614
  _internalInit(init?: ComponentInit<this>) {
1019
615
  if (typeof init === "object") {
1020
616
  for (const key of Object.keys(init)) {
@@ -1094,10 +690,7 @@
1094
690
  destroyComponentInstance(this as any);
1095
691
  }
1096
692
 
1097
- /**
1098
- * Controls whether this component is enabled
1099
- * Disabled components don't receive lifecycle callbacks
1100
- */
693
+
1101
694
  get enabled(): boolean {
1102
695
  return typeof this.__isEnabled === "boolean" ? this.__isEnabled : true; // if it has no enabled field it is always enabled
1103
696
  }
@@ -1127,154 +720,85 @@
1127
720
  }
1128
721
  }
1129
722
 
1130
- /**
1131
- * Gets the position of this component's GameObject in world space
1132
- */
1133
723
  get worldPosition(): Vector3 {
1134
724
  return threeutils.getWorldPosition(this.gameObject);
1135
725
  }
1136
726
 
1137
- /**
1138
- * Sets the position of this component's GameObject in world space
1139
- * @param val The world position vector to set
1140
- */
1141
727
  set worldPosition(val: Vector3) {
1142
728
  threeutils.setWorldPosition(this.gameObject, val);
1143
729
  }
1144
730
 
1145
- /**
1146
- * Sets the position of this component's GameObject in world space using individual coordinates
1147
- * @param x X-coordinate in world space
1148
- * @param y Y-coordinate in world space
1149
- * @param z Z-coordinate in world space
1150
- */
1151
731
  setWorldPosition(x: number, y: number, z: number) {
1152
732
  threeutils.setWorldPositionXYZ(this.gameObject, x, y, z);
1153
733
  }
1154
734
 
1155
- /**
1156
- * Gets the rotation of this component's GameObject in world space as a quaternion
1157
- */
735
+
1158
736
  get worldQuaternion(): Quaternion {
1159
737
  return threeutils.getWorldQuaternion(this.gameObject);
1160
738
  }
1161
-
1162
- /**
1163
- * Sets the rotation of this component's GameObject in world space using a quaternion
1164
- * @param val The world rotation quaternion to set
1165
- */
1166
739
  set worldQuaternion(val: Quaternion) {
1167
740
  threeutils.setWorldQuaternion(this.gameObject, val);
1168
741
  }
1169
-
1170
- /**
1171
- * Sets the rotation of this component's GameObject in world space using quaternion components
1172
- * @param x X component of the quaternion
1173
- * @param y Y component of the quaternion
1174
- * @param z Z component of the quaternion
1175
- * @param w W component of the quaternion
1176
- */
1177
742
  setWorldQuaternion(x: number, y: number, z: number, w: number) {
1178
743
  threeutils.setWorldQuaternionXYZW(this.gameObject, x, y, z, w);
1179
744
  }
1180
745
 
1181
- /**
1182
- * Gets the rotation of this component's GameObject in world space as Euler angles (in radians)
1183
- */
746
+ // world euler (in radians)
1184
747
  get worldEuler(): Euler {
1185
748
  return threeutils.getWorldEuler(this.gameObject);
1186
749
  }
1187
750
 
1188
- /**
1189
- * Sets the rotation of this component's GameObject in world space using Euler angles (in radians)
1190
- * @param val The world rotation Euler angles to set
1191
- */
751
+ // world euler (in radians)
1192
752
  set worldEuler(val: Euler) {
1193
753
  threeutils.setWorldEuler(this.gameObject, val);
1194
754
  }
1195
755
 
1196
- /**
1197
- * Gets the rotation of this component's GameObject in world space as Euler angles (in degrees)
1198
- */
756
+ // returns rotation in degrees
1199
757
  get worldRotation(): Vector3 {
1200
- return this.gameObject.worldRotation;
758
+ return this.gameObject.worldRotation;;
1201
759
  }
1202
760
 
1203
- /**
1204
- * Sets the rotation of this component's GameObject in world space using Euler angles (in degrees)
1205
- * @param val The world rotation vector to set (in degrees)
1206
- */
1207
761
  set worldRotation(val: Vector3) {
1208
762
  this.setWorldRotation(val.x, val.y, val.z, true);
1209
763
  }
1210
764
 
1211
- /**
1212
- * Sets the rotation of this component's GameObject in world space using individual Euler angles
1213
- * @param x X-axis rotation
1214
- * @param y Y-axis rotation
1215
- * @param z Z-axis rotation
1216
- * @param degrees Whether the values are in degrees (true) or radians (false)
1217
- */
1218
765
  setWorldRotation(x: number, y: number, z: number, degrees: boolean = true) {
1219
766
  threeutils.setWorldRotationXYZ(this.gameObject, x, y, z, degrees);
1220
767
  }
1221
768
 
1222
769
  private static _forward: Vector3 = new Vector3();
1223
- /**
1224
- * Gets the forward direction vector (0,0,-1) of this component's GameObject in world space
1225
- */
770
+ /** Forward (0,0,-1) vector in world space */
1226
771
  public get forward(): Vector3 {
1227
772
  return Component._forward.set(0, 0, -1).applyQuaternion(this.worldQuaternion);
1228
773
  }
1229
774
  private static _right: Vector3 = new Vector3();
1230
- /**
1231
- * Gets the right direction vector (1,0,0) of this component's GameObject in world space
1232
- */
775
+ /** Right (1,0,0) vector in world space */
1233
776
  public get right(): Vector3 {
1234
777
  return Component._right.set(1, 0, 0).applyQuaternion(this.worldQuaternion);
1235
778
  }
1236
779
  private static _up: Vector3 = new Vector3();
1237
- /**
1238
- * Gets the up direction vector (0,1,0) of this component's GameObject in world space
1239
- */
780
+ /** Up (0,1,0) vector in world space */
1240
781
  public get up(): Vector3 {
1241
782
  return Component._up.set(0, 1, 0).applyQuaternion(this.worldQuaternion);
1242
783
  }
1243
784
 
785
+
786
+
1244
787
  // EventTarget implementation:
1245
788
 
1246
- /**
1247
- * Storage for event listeners registered to this component
1248
- * @private
1249
- */
1250
789
  private _eventListeners = new Map<string, EventListener[]>();
1251
790
 
1252
- /**
1253
- * Registers an event listener for the specified event type
1254
- * @param type The event type to listen for
1255
- * @param listener The callback function to execute when the event occurs
1256
- */
1257
791
  addEventListener<T extends Event>(type: string, listener: (evt: T) => any) {
1258
792
  this._eventListeners[type] = this._eventListeners[type] || [];
1259
793
  this._eventListeners[type].push(listener);
1260
794
  }
1261
795
 
1262
- /**
1263
- * Removes a previously registered event listener
1264
- * @param type The event type the listener was registered for
1265
- * @param listener The callback function to remove
1266
- */
1267
796
  removeEventListener<T extends Event>(type: string, listener: (arg: T) => any) {
1268
797
  if (!this._eventListeners[type]) return;
1269
798
  const index = this._eventListeners[type].indexOf(listener);
1270
799
  if (index >= 0) this._eventListeners[type].splice(index, 1);
1271
800
  }
1272
801
 
1273
- /**
1274
- * Dispatches an event to all registered listeners
1275
- * @param evt The event object to dispatch
1276
- * @returns Always returns false (standard implementation of EventTarget)
1277
- */
1278
802
  dispatchEvent(evt: Event): boolean {
1279
803
  if (!evt || !this._eventListeners[evt.type]) return false;
1280
804
  const listeners = this._eventListeners[evt.type];
src/engine-components/DragControls.ts CHANGED
@@ -18,10 +18,8 @@
18
18
  import type { IPointerEventHandler, PointerEventData } from "./ui/PointerEvents.js";
19
19
  import { ObjectRaycaster } from "./ui/Raycaster.js";
20
20
 
21
- /** Enable debug visualization and logging for DragControls by using the URL parameter `?debugdrag`. */
22
21
  const debug = getParam("debugdrag");
23
22
 
24
- /** Buffer to store currently active DragControls components */
25
23
  const dragControlsBuffer: DragControls[] = [];
26
24
 
27
25
  /**
@@ -36,7 +34,7 @@
36
34
  HitNormal = 2,
37
35
  /** Combination of XZ and Screen based on the viewing angle. Low angles result in Screen dragging and higher angles in XZ dragging. */
38
36
  DynamicViewAngle = 3,
39
- /** The drag plane is snapped to surfaces in the scene while dragging. */
37
+ /** The drag plane is adjusted dynamically while dragging. */
40
38
  SnapToSurfaces = 4,
41
39
  /** Don't allow dragging the object */
42
40
  None = 5,
@@ -44,24 +42,18 @@
44
42
 
45
43
  /**
46
44
  * DragControls allows you to drag objects around in the scene. It can be used to move objects in 2D (screen space) or 3D (world space).
47
- * Debug mode can be enabled with the URL parameter `?debugdrag`, which shows visual helpers and logs drag operations.
48
- *
49
45
  * @category Interactivity
50
46
  * @group Components
51
47
  */
52
48
  export class DragControls extends Behaviour implements IPointerEventHandler {
53
49
 
54
50
  /**
55
- * Checks if any DragControls component is currently active with selected objects
56
51
  * @returns True if any DragControls component is currently active
57
52
  */
58
53
  public static get HasAnySelected(): boolean { return this._active > 0; }
59
54
  private static _active: number = 0;
60
55
 
61
- /**
62
- * Retrieves a list of all DragControl components that are currently dragging objects.
63
- * @returns Array of currently active DragControls components
64
- */
56
+ /** @returns a list of DragControl components that are currently active */
65
57
  public static get CurrentlySelected() {
66
58
  dragControlsBuffer.length = 0;
67
59
  for (const dc of this._instances) {
@@ -71,71 +63,51 @@
71
63
  }
72
64
  return dragControlsBuffer;
73
65
  }
74
- /** Registry of currently active and enabled DragControls components */
66
+ /** Currently active and enabled DragControls components */
75
67
  private static _instances: DragControls[] = [];
76
68
 
77
- /**
78
- * Determines how and where the object is dragged along. Different modes include
79
- * dragging along a plane, attached to the pointer, or following surface normals.
80
- */
69
+
70
+
71
+ // dragPlane (floor, object, view)
72
+ // snap to surface (snap orientation?)
73
+ // two-handed drag (scale, rotate, move)
74
+ // keep upright (no tilt)
75
+
76
+ /** How and where the object is dragged along. */
81
77
  @serializable()
82
78
  public dragMode: DragMode = DragMode.DynamicViewAngle;
83
79
 
84
- /**
85
- * Snaps dragged objects to a 3D grid with the specified resolution.
86
- * Set to 0 to disable snapping.
87
- */
80
+ /** Snap dragged objects to a XYZ grid – 0 means: no snapping. */
88
81
  @serializable()
89
82
  public snapGridResolution: number = 0.0;
90
83
 
91
- /**
92
- * When true, maintains the original rotation of the dragged object while moving it.
93
- * When false, allows the object to rotate freely during dragging.
94
- */
84
+ /** Keep the original rotation of the dragged object. */
95
85
  @serializable()
96
86
  public keepRotation: boolean = true;
97
87
 
98
- /**
99
- * Determines how and where the object is dragged along while dragging in XR.
100
- * Uses a separate setting from regular drag mode for better XR interaction.
101
- */
88
+ /** How and where the object is dragged along while dragging in XR. */
102
89
  @serializable()
103
90
  public xrDragMode: DragMode = DragMode.Attached;
104
91
 
105
- /**
106
- * When true, maintains the original rotation of the dragged object during XR dragging.
107
- * When false, allows the object to rotate freely during XR dragging.
108
- */
92
+ /** Keep the original rotation of the dragged object while dragging in XR. */
109
93
  @serializable()
110
94
  public xrKeepRotation: boolean = false;
111
95
 
112
- /**
113
- * Multiplier that affects how quickly objects move closer or further away when dragging in XR.
114
- * Higher values make distance changes more pronounced.
115
- * This is similar to mouse acceleration on a screen.
116
- */
96
+ /** Accelerate dragging objects closer / further away when in XR */
117
97
  @serializable()
118
98
  public xrDistanceDragFactor: number = 1;
119
99
 
120
- /**
121
- * When enabled, draws a visual line from the dragged object downwards to the next raycast hit,
122
- * providing visual feedback about the object's position relative to surfaces below it.
123
- */
100
+ /** When enabled, draws a line from the dragged object downwards to the next raycast hit. */
124
101
  @serializable()
125
102
  public showGizmo: boolean = false;
126
103
 
127
- /**
128
- * Returns the object currently being dragged by this DragControls component, if any.
129
- * @returns The object being dragged or null if no object is currently dragged
130
- */
104
+ /** The currently dragged object (if any) */
131
105
  get draggedObject() {
132
106
  return this._targetObject;
133
107
  }
134
108
 
135
109
  /**
136
- * Updates the object that is being dragged by the DragControls.
137
- * This can be used to change the target during a drag operation.
138
- * @param obj The new object to drag, or null to stop dragging
110
+ * Use to update the object that is being dragged by the DragControls
139
111
  */
140
112
  setTargetObject(obj: Object3D | null) {
141
113
  this._targetObject = obj;
@@ -161,8 +133,8 @@
161
133
  this._rigidbody[wasKinematicKey] = false;
162
134
  }
163
135
  }
136
+
164
137
  }
165
-
166
138
  private _rigidbody: Rigidbody | null = null;
167
139
 
168
140
  // future:
@@ -210,20 +182,11 @@
210
182
  DragControls._instances = DragControls._instances.filter(i => i !== this);
211
183
  }
212
184
 
213
- /**
214
- * Checks if editing is allowed for the current networking connection.
215
- * @param _obj Optional object to check edit permissions for
216
- * @returns True if editing is allowed
217
- */
218
185
  private allowEdit(_obj: Object3D | null = null) {
219
186
  return this.context.connection.allowEditing;
220
187
  }
221
188
 
222
- /**
223
- * Handles pointer enter events. Sets the cursor style and tracks the hovered object.
224
- * @param evt Pointer event data containing information about the interaction
225
- * @internal
226
- */
189
+ /** @internal */
227
190
  onPointerEnter?(evt: PointerEventData) {
228
191
  if (!this.allowEdit(this.gameObject)) return;
229
192
  if (evt.mode !== "screen") return;
@@ -239,20 +202,12 @@
239
202
  this.context.domElement.style.cursor = 'pointer';
240
203
  }
241
204
 
242
- /**
243
- * Handles pointer movement events. Marks the event as used if dragging is active.
244
- * @param args Pointer event data containing information about the movement
245
- * @internal
246
- */
205
+ /** @internal */
247
206
  onPointerMove?(args: PointerEventData) {
248
207
  if (this._isDragging || this._potentialDragStartEvt !== null) args.use();
249
208
  }
250
209
 
251
- /**
252
- * Handles pointer exit events. Resets the cursor style when the pointer leaves a draggable object.
253
- * @param evt Pointer event data containing information about the interaction
254
- * @internal
255
- */
210
+ /** @internal */
256
211
  onPointerExit?(evt: PointerEventData) {
257
212
  if (!this.allowEdit(this.gameObject)) return;
258
213
  if (evt.mode !== "screen") return;
@@ -260,11 +215,7 @@
260
215
  this.context.domElement.style.cursor = 'auto';
261
216
  }
262
217
 
263
- /**
264
- * Handles pointer down events. Initiates the potential drag operation if conditions are met.
265
- * @param args Pointer event data containing information about the interaction
266
- * @internal
267
- */
218
+ /** @internal */
268
219
  onPointerDown(args: PointerEventData) {
269
220
  if (!this.allowEdit(this.gameObject)) return;
270
221
  if (args.used) return;
@@ -311,11 +262,7 @@
311
262
  }
312
263
  }
313
264
 
314
- /**
315
- * Handles pointer up events. Finalizes or cancels the drag operation.
316
- * @param args Pointer event data containing information about the interaction
317
- * @internal
318
- */
265
+ /** @internal */
319
266
  onPointerUp(args: PointerEventData) {
320
267
  if (debug) Gizmos.DrawLabel(args.point ?? this.gameObject.worldPosition, "POINTERUP:" + args.pointerId + ", " + args.button, .03, 3);
321
268
  if (!this.allowEdit(this.gameObject)) return;
@@ -346,12 +293,9 @@
346
293
  }
347
294
  }
348
295
 
349
- /**
350
- * Updates the drag operation every frame. Processes pointer movement, accumulates drag distance
351
- * and triggers drag start once there's enough movement.
352
- * @internal
353
- */
296
+ /** @internal */
354
297
  update(): void {
298
+
355
299
  for (const handler of this._dragHandlers.values()) {
356
300
  if (handler.collectMovementInfo) handler.collectMovementInfo();
357
301
  // TODO this doesn't make sense, we should instead just use the max here
@@ -380,12 +324,7 @@
380
324
  this.onAnyDragUpdate();
381
325
  }
382
326
 
383
- /**
384
- * Called when the first pointer starts dragging on this object.
385
- * Sets up network synchronization and marks rigidbodies for dragging.
386
- * Not called for subsequent pointers on the same object.
387
- * @param evt Pointer event data that initiated the drag
388
- */
327
+ /** Called when the first pointer starts dragging on this object. Not called for subsequent pointers on the same object. */
389
328
  private onFirstDragStart(evt: PointerEventData) {
390
329
  if (!evt || !evt.object) return;
391
330
 
@@ -417,10 +356,7 @@
417
356
  this._draggingRigidbodies.push(...rbs);
418
357
  }
419
358
 
420
- /**
421
- * Called each frame as long as any pointer is dragging this object.
422
- * Updates visuals and keeps rigidbodies awake during the drag.
423
- */
359
+ /** Called each frame as long as any pointer is dragging this object. */
424
360
  private onAnyDragUpdate() {
425
361
  if (!this._dragHelper) return;
426
362
  this._dragHelper.showGizmo = this.showGizmo;
@@ -437,11 +373,7 @@
437
373
  InstancingUtil.markDirty(object);
438
374
  }
439
375
 
440
- /**
441
- * Called when the last pointer has been removed from this object.
442
- * Cleans up drag state and applies final velocities to rigidbodies.
443
- * @param evt Pointer event data for the last pointer that was lifted
444
- */
376
+ /** Called when the last pointer has been removed from this object. */
445
377
  private onLastDragEnd(evt: PointerEventData | null) {
446
378
  if (!this || !this._isDragging) return;
447
379
  this._isDragging = false;
@@ -467,10 +399,7 @@
467
399
  }
468
400
  }
469
401
 
470
- /**
471
- * Common interface for pointer handlers (single touch and multi touch).
472
- * Defines methods for tracking movement and managing target objects during drag operations.
473
- */
402
+ /** Common interface for pointer handlers (single touch and multi touch) */
474
403
  interface IDragHandler {
475
404
  /** Used to determine if a drag has happened for this handler */
476
405
  getTotalMovement?(): Vector3;
@@ -485,10 +414,7 @@
485
414
  onDragUpdate?(numberOfPointers: number): void;
486
415
  }
487
416
 
488
- /**
489
- * Handles two touch points affecting one object.
490
- * Enables multi-touch interactions that allow movement, scaling, and rotation of objects.
491
- */
417
+ /** Handles two touch points affecting one object. Allows movement, scale and rotation of objects. */
492
418
  class MultiTouchDragHandler implements IDragHandler {
493
419
 
494
420
  handlerA: DragPointerHandler;
@@ -734,27 +660,15 @@
734
660
  }
735
661
 
736
662
 
737
- /**
738
- * Handles a single pointer on an object.
739
- * DragPointerHandlers manage determining if a drag operation has started, tracking pointer movement,
740
- * and controlling object translation based on the drag mode.
663
+ /** Handles a single pointer on an object. DragPointerHandlers are created on pointer enter,
664
+ * help with determining if there's actually a drag, and then perform operations based on spatial pointer data.
741
665
  */
742
666
  class DragPointerHandler implements IDragHandler {
743
667
 
744
- /**
745
- * Returns the accumulated movement of the pointer in world units.
746
- * Used for determining if enough motion has occurred to start a drag.
747
- */
668
+ /** Absolute movement of the pointer. Used for determining if a motion/drag is happening.
669
+ * This is in world units, so very small for screens (near-plane space change) */
748
670
  getTotalMovement(): Vector3 { return this._totalMovement; }
749
-
750
- /**
751
- * Returns the object that follows the pointer during dragging operations.
752
- */
753
671
  get followObject(): GameObject { return this._followObject; }
754
-
755
- /**
756
- * Returns the point where the pointer initially hit the object in local space.
757
- */
758
672
  get hitPointInLocalSpace(): Vector3 { return this._hitPointInLocalSpace; }
759
673
 
760
674
  private context: Context;
@@ -1335,28 +1249,18 @@
1335
1249
  }
1336
1250
  }
1337
1251
 
1338
- /**
1339
- * Provides visual helper elements for DragControls.
1340
- * Shows where objects will be placed and their relation to surfaces below them.
1252
+ /** Currently does _only_ provide visuals support for DragControls operations.
1253
+ * Previously it also provided the actual drag functionality, but that has been moved to DragControls for now.
1341
1254
  */
1342
1255
  class LegacyDragVisualsHelper {
1343
1256
 
1344
- /** Controls whether visual helpers like lines and markers are displayed */
1345
1257
  showGizmo: boolean = true;
1346
-
1347
- /** When true, drag plane alignment changes based on view angle */
1348
1258
  useViewAngle: boolean = true;
1349
1259
 
1350
- /**
1351
- * Checks if there is a currently selected object being visualized
1352
- */
1353
1260
  public get hasSelected(): boolean {
1354
1261
  return this._selected !== null && this._selected !== undefined;
1355
1262
  }
1356
1263
 
1357
- /**
1358
- * Returns the currently selected object being visualized, if any
1359
- */
1360
1264
  public get selected(): Object3D | null {
1361
1265
  return this._selected;
1362
1266
  }
src/engine-components/DropListener.ts CHANGED
@@ -19,104 +19,63 @@
19
19
  import { Behaviour } from "./Component.js";
20
20
  import { EventList } from "./EventList.js";
21
21
 
22
- /**
23
- * Debug mode can be enabled with the URL parameter `?debugdroplistener`, which
24
- * logs additional information during drag and drop events and visualizes hit points.
25
- */
26
22
  const debug = getParam("debugdroplistener");
27
23
 
28
- /**
29
- * Events dispatched by the DropListener component
30
- * @enum {string}
31
- */
32
24
  export enum DropListenerEvents {
33
25
  /**
34
- * Dispatched when a file is dropped into the scene. The detail of the event is the {@link File} that was dropped.
35
- * The event is called once for each dropped file.
26
+ * Dispatched when a file is dropped into the scene. The detail of the event is the file that was dropped.
36
27
  */
37
28
  FileDropped = "file-dropped",
38
29
  /**
39
- * Dispatched when a new object is added to the scene. The detail of the event contains {@link DropListenerOnDropArguments} for the content that was added.
30
+ * Dispatched when a new object is added to the scene. The detail of the event is the glTF that was added.
40
31
  */
41
32
  ObjectAdded = "object-added",
42
33
  }
43
34
 
44
- /**
45
- * Context information for a drop operation
46
- */
47
35
  declare type DropContext = {
48
- /** Position where the file was dropped in screen coordinates */
49
36
  screenposition: Vector2;
50
- /** URL of the dropped content, if applicable */
51
37
  url?: string,
52
- /** File object of the dropped content, if applicable */
53
38
  file?: File;
54
- /** 3D position where the content should be placed */
55
39
  point?: Vec3;
56
- /** Size dimensions for the content */
57
40
  size?: Vec3;
58
41
  }
59
42
 
60
43
 
61
- /**
62
- * Network event arguments passed between clients when using the DropListener with networking
63
- */
44
+ /** Networking event arguments for the DropListener component */
64
45
  export declare type DropListenerNetworkEventArguments = {
65
- /** Unique identifier of the sender */
66
46
  guid: string,
67
- /** Name of the dropped object */
68
47
  name: string,
69
- /** URL or array of URLs to the dropped content */
70
48
  url: string | string[],
71
49
  /** Worldspace point where the object was placed in the scene */
72
50
  point: Vec3;
73
51
  /** Bounding box size */
74
52
  size: Vec3;
75
- /** MD5 hash of the content for verification */
76
53
  contentMD5: string;
77
54
  }
78
55
 
79
- /**
80
- * Arguments provided to handlers when an object is dropped or added to the scene
81
- */
82
56
  export declare type DropListenerOnDropArguments = {
83
- /** The DropListener component that processed the drop event */
84
57
  sender: DropListener,
85
- /** The root object added to the scene */
58
+ /** the root object added to the scene */
86
59
  object: Object3D,
87
- /** The complete model with all associated data */
60
+ /** The whole dropped model */
88
61
  model: Model,
89
- /** MD5 hash of the content for verification */
90
62
  contentMD5: string;
91
- /** The original dropped URL or File object */
92
63
  dropped: URL | File | undefined;
93
64
  }
94
65
 
95
- /**
96
- * CustomEvent dispatched when an object is added to the scene via the DropListener
97
- */
66
+ /** Dispatched when an object is dropped/changed */
98
67
  class DropListenerAddedEvent<T extends DropListenerOnDropArguments> extends CustomEvent<T> {
99
- /**
100
- * Creates a new added event with the provided details
101
- * @param detail Information about the added object
102
- */
103
68
  constructor(detail: T) {
104
69
  super(DropListenerEvents.ObjectAdded, { detail });
105
70
  }
106
71
  }
107
72
 
108
- /**
109
- * Key name used for blob storage parameters
110
- */
111
73
  const blobKeyName = "blob";
112
74
 
113
75
  /** The DropListener component is used to listen for drag and drop events in the browser and add the dropped files to the scene
114
76
  * It can be used to allow users to drag and drop glTF files into the scene to add new objects.
115
77
  *
116
- * If {@link useNetworking} is enabled, the DropListener will automatically synchronize dropped files to other connected clients.
117
- * Enable {@link fitIntoVolume} to automatically scale dropped objects to fit within the volume defined by {@link fitVolumeSize}.
118
- *
119
- * The following events are dispatched by the DropListener:
78
+ * ## Events
120
79
  * - **object-added** - dispatched when a new object is added to the scene
121
80
  * - **file-dropped** - dispatched when a file is dropped into the scene
122
81
  *
@@ -144,46 +103,42 @@
144
103
  export class DropListener extends Behaviour {
145
104
 
146
105
  /**
147
- * When enabled, the DropListener will automatically synchronize dropped files to other connected clients.
148
- * When a file is dropped locally, it will be uploaded to blob storage and the URL will be shared with other clients.
106
+ * When enabled the DropListener will automatically network dropped files to other clients.
149
107
  */
150
108
  @serializable()
151
109
  useNetworking: boolean = true;
152
110
 
153
111
  /**
154
- * When assigned, the DropListener will only accept files that are dropped on this specific object.
155
- * This allows creating designated drop zones in your scene.
112
+ * When assigned the Droplistener will only accept files that are dropped on this object.
156
113
  */
157
114
  @serializable(Object3D)
158
115
  dropArea?: Object3D;
159
116
 
160
117
  /**
161
- * When enabled, dropped objects will be automatically scaled to fit within the volume defined by fitVolumeSize.
162
- * Useful for ensuring dropped models appear at an appropriate scale.
118
+ * When enabled the object will be fitted into a volume. Use {@link fitVolumeSize} to specify the volume size.
163
119
  * @default false
164
120
  */
165
121
  @serializable()
166
122
  fitIntoVolume: boolean = false;
167
123
 
168
124
  /**
169
- * Defines the dimensions of the volume that dropped objects will be scaled to fit within.
170
- * Only used when fitIntoVolume is enabled.
125
+ * The volume size will be used to fit the object into the volume. Use {@link fitIntoVolume} to enable this feature.
171
126
  */
172
127
  @serializable(Vector3)
173
128
  fitVolumeSize = new Vector3(1, 1, 1);
174
129
 
175
- /**
176
- * When enabled, dropped objects will be positioned at the point where the cursor hit the scene.
177
- * When disabled, objects will be placed at the origin of the DropListener.
130
+ /** When enabled the object will be placed at the drop position (under the cursor)
178
131
  * @default true
179
132
  */
180
133
  @serializable()
181
134
  placeAtHitPosition: boolean = true;
182
135
 
136
+
183
137
  /**
184
- * Event list that gets invoked after a file has been successfully added to the scene.
185
- * Receives {@link DropListenerOnDropArguments} containing the added object and related information.
138
+ * Invoked after a file has been **added** to the scene.
139
+ * Arguments are {@link DropListenerOnDropArguments}
186
140
  * @event object-added
141
+ * @param {DropListenerOnDropArguments} evt
187
142
  * @example
188
143
  * ```typescript
189
144
  * dropListener.onDropped.addEventListener((evt) => {
@@ -223,10 +178,6 @@
223
178
  this.removePreviouslyAddedObjects(false);
224
179
  }
225
180
 
226
- /**
227
- * Handles network events received from other clients containing information about dropped objects
228
- * @param evt Network event data containing object information, position, and content URL
229
- */
230
181
  private onNetworkEvent = (evt: DropListenerNetworkEventArguments) => {
231
182
  if (!this.useNetworking) {
232
183
  if (debug) console.debug("[DropListener] Ignoring networked event because networking is disabled", evt);
@@ -248,11 +199,6 @@
248
199
  }
249
200
  }
250
201
 
251
- /**
252
- * Handles clipboard paste events and processes them as potential URL drops
253
- * Only URLs are processed by this handler, and only when editing is allowed
254
- * @param evt The paste event
255
- */
256
202
  private handlePaste = (evt: Event) => {
257
203
  if (this.context.connection.allowEditing === false) return;
258
204
  if (evt.defaultPrevented) return;
@@ -271,22 +217,12 @@
271
217
  .catch(console.warn);
272
218
  }
273
219
 
274
- /**
275
- * Handles drag events over the renderer's canvas
276
- * Prevents default behavior to enable drop events
277
- * @param evt The drag event
278
- */
279
220
  private onDrag = (evt: DragEvent) => {
280
221
  if (this.context.connection.allowEditing === false) return;
281
222
  // necessary to get drop event
282
223
  evt.preventDefault();
283
224
  }
284
225
 
285
- /**
286
- * Processes drop events to add files to the scene
287
- * Handles both file drops and text/URL drops
288
- * @param evt The drop event
289
- */
290
226
  private onDrop = async (evt: DragEvent) => {
291
227
  if (this.context.connection.allowEditing === false) return;
292
228
 
@@ -330,14 +266,6 @@
330
266
  }
331
267
  }
332
268
 
333
- /**
334
- * Processes a dropped or pasted URL and tries to load it as a 3D model
335
- * Handles special cases like GitHub URLs and Polyhaven asset URLs
336
- * @param url The URL to process
337
- * @param ctx Context information about where the drop occurred
338
- * @param isRemote Whether this URL was shared from a remote client
339
- * @returns The added object or null if loading failed
340
- */
341
269
  private async addFromUrl(url: string, ctx: DropContext, isRemote: boolean) {
342
270
  if (debug) console.log("dropped url", url);
343
271
 
@@ -387,12 +315,6 @@
387
315
 
388
316
  private _abort: AbortController | null = null;
389
317
 
390
- /**
391
- * Processes dropped files, loads them as 3D models, and handles networking if enabled
392
- * Creates an abort controller to cancel previous uploads if new files are dropped
393
- * @param fileList Array of dropped files
394
- * @param ctx Context information about where the drop occurred
395
- */
396
318
  private async addDroppedFiles(fileList: Array<File>, ctx: DropContext) {
397
319
  if (debug) console.log("Add files", fileList)
398
320
  if (!Array.isArray(fileList)) return;
@@ -439,10 +361,7 @@
439
361
  private readonly _addedObjects = new Array<Object3D>();
440
362
  private readonly _addedModels = new Array<Model>();
441
363
 
442
- /**
443
- * Removes all previously added objects from the scene
444
- * @param doDestroy When true, destroys the objects; when false, just clears the references
445
- */
364
+ /** Removes all previously added objects from the scene and removes those object references */
446
365
  private removePreviouslyAddedObjects(doDestroy: boolean = true) {
447
366
  if (doDestroy) {
448
367
  for (const prev of this._addedObjects) {
@@ -456,13 +375,7 @@
456
375
  }
457
376
 
458
377
  /**
459
- * Adds a loaded model to the scene with proper positioning and scaling.
460
- * Handles placement based on component settings and raycasting.
461
- * If {@link fitIntoVolume} is enabled, the object will be scaled to fit within the volume defined by {@link fitVolumeSize}.
462
- * @param data The loaded model data and content hash
463
- * @param ctx Context information about where the drop occurred
464
- * @param isRemote Whether this object was shared from a remote client
465
- * @returns The added object or null if adding failed
378
+ * Adds the object to the scene and fits it into the volume if {@link fitIntoVolume} is enabled.
466
379
  */
467
380
  private addObject(data: { model: Model, contentMD5: string }, ctx: DropContext, isRemote: boolean): Object3D | null {
468
381
 
@@ -531,13 +444,6 @@
531
444
  return obj;
532
445
  }
533
446
 
534
- /**
535
- * Sends a network event to other clients about a dropped object
536
- * Only triggered when networking is enabled and the connection is established
537
- * @param url The URL to the content that was dropped
538
- * @param obj The object that was added to the scene
539
- * @param contentmd5 The content hash for verification
540
- */
541
447
  private async sendDropEvent(url: string, obj: Object3D, contentmd5: string) {
542
448
  if (!this.useNetworking) {
543
449
  if (debug) console.debug("[DropListener] Ignoring networked event because networking is disabled", url);
@@ -557,20 +463,12 @@
557
463
  this.context.connection.send("droplistener", evt);
558
464
  }
559
465
  }
560
-
561
- /**
562
- * Deletes remote state for this DropListener's objects
563
- * Called when new files are dropped to clean up previous state
564
- */
565
466
  private deleteDropEvent() {
566
467
  this.context.connection.sendDeleteRemoteState(this.guid);
567
468
  }
568
469
 
569
- /**
570
- * Tests if a drop event occurred within the designated drop area if one is specified
571
- * @param ctx The drop context containing screen position information
572
- * @returns True if the drop is valid (either no drop area is set or the drop occurred inside it)
573
- */
470
+
471
+
574
472
  private testIfIsInDropArea(ctx: DropContext): boolean {
575
473
  if (this.dropArea) {
576
474
  const screenPoint = this.context.input.convertScreenspaceToRaycastSpace(ctx.screenposition.clone());
@@ -594,11 +492,7 @@
594
492
 
595
493
  }
596
494
 
597
- /**
598
- * Attempts to convert a Polyhaven website URL to a direct glTF model download URL
599
- * @param urlStr The original Polyhaven URL
600
- * @returns The direct download URL for the glTF model if it's a valid Polyhaven asset URL, otherwise returns the original URL
601
- */
495
+
602
496
  function tryResolvePolyhavenAssetUrl(urlStr: string) {
603
497
  if (!urlStr.startsWith("https://polyhaven.com/")) return urlStr;
604
498
  // Handle dropping polyhaven image url
@@ -612,18 +506,10 @@
612
506
  return assetUrl;
613
507
  }
614
508
 
615
- /**
616
- * Helper namespace for loading files and models from various sources
617
- */
509
+
510
+
618
511
  namespace FileHelper {
619
512
 
620
- /**
621
- * Loads and processes a File object into a 3D model
622
- * @param file The file to load (supported formats: gltf, glb, fbx, obj, usdz, vrm)
623
- * @param context The application context
624
- * @param args Additional arguments including a unique guid for instantiation
625
- * @returns Promise containing the loaded model and its content hash, or null if loading failed
626
- */
627
513
  export async function loadFile(file: File, context: Context, args: { guid: string }): Promise<{ model: Model, contentMD5: string } | null> {
628
514
  const name = file.name.toLowerCase();
629
515
  if (name.endsWith(".gltf") ||
@@ -658,12 +544,6 @@
658
544
  return null;
659
545
  }
660
546
 
661
- /**
662
- * Loads a 3D model from a URL with progress visualization
663
- * @param url The URL to load the model from
664
- * @param args Arguments including context, parent object, and optional placement information
665
- * @returns Promise containing the loaded model and its content hash, or null if loading failed
666
- */
667
547
  export async function loadFileFromURL(url: URL, args: { guid: string, context: Context, parent: Object3D, point?: Vec3, size?: Vec3 }): Promise<{ model: Model, contentMD5: string } | null> {
668
548
  return new Promise(async (resolve, _reject) => {
669
549
 
src/engine/engine_addressables.ts CHANGED
@@ -164,11 +164,6 @@
164
164
  return this._url;
165
165
  }
166
166
 
167
- /** The name of the assigned url. This name is deduced from the url and might not reflect the actual name of the asset */
168
- get urlName(): string {
169
- return this._urlName;
170
- };
171
-
172
167
  /**
173
168
  * @returns true if the uri is a valid URL (http, https, blob)
174
169
  */
@@ -185,7 +180,6 @@
185
180
  private _asset: any;
186
181
  private _glbRoot?: Object3D | null;
187
182
  private _url: string;
188
- private _urlName: string;
189
183
  private _progressListeners: ProgressCallback[] = [];
190
184
 
191
185
  private _hash?: string;
@@ -197,27 +191,12 @@
197
191
  /** @internal */
198
192
  constructor(uri: string, hash?: string, asset: any = null) {
199
193
  this._url = uri;
200
-
201
- const lastUriPart = uri.lastIndexOf("/");
202
- if (lastUriPart >= 0) {
203
- this._urlName = uri.substring(lastUriPart + 1);
204
- // remove file extension
205
- const lastDot = this._urlName.lastIndexOf(".");
206
- if (lastDot >= 0) {
207
- this._urlName = this._urlName.substring(0, lastDot);
208
- }
209
- }
210
- else {
211
- this._urlName = uri;
212
- }
213
-
214
194
  this._hash = hash;
215
195
  if (uri.includes("?v="))
216
196
  this._hashedUri = uri;
217
197
  else
218
198
  this._hashedUri = hash ? uri + "?v=" + hash : uri;
219
199
  if (asset !== null) this.asset = asset;
220
-
221
200
  registerPrefabProvider(this._url, this.onResolvePrefab.bind(this));
222
201
  }
223
202
 
@@ -548,20 +527,10 @@
548
527
 
549
528
  private loader: TextureLoader | null = null;
550
529
  createTexture(): Promise<Texture | null> {
551
- if (!this.url) {
552
- console.error("Can not load texture without url");
553
- return failedTexturePromise;
554
- }
555
-
530
+ if (!this.url) return failedTexturePromise;
556
531
  if (!this.loader) this.loader = new TextureLoader();
557
532
  this.loader.setCrossOrigin("anonymous");
558
- return this.loader.loadAsync(this.url).then(res => {
559
- if (res && !res.name?.length) {
560
- // default name if no name is defined
561
- res.name = this.url.split("/").pop() ?? this.url;
562
- }
563
- return res;
564
- })
533
+ return this.loader.loadAsync(this.url);
565
534
  // return this.getBitmap().then((bitmap) => {
566
535
  // if (bitmap) {
567
536
  // const texture = new Texture(bitmap);
src/engine/engine_context.ts CHANGED
@@ -864,7 +864,7 @@
864
864
  if (name.length > 3) offendingComponentName = name;
865
865
  }
866
866
  }
867
- console.error(`Needle Engine dependencies failed to load:\n\n# Make sure you don't have circular imports in your scripts!\n\nPossible solutions: \n→ Replace @serializable(${offendingComponentName}) in your script with @serializable(Behaviour)\n→ If you only need type information try importing the type only, e.g: import { type ${offendingComponentName} }\n\n---`, err)
867
+ console.error(`Needle Engine dependencies failed to load:\n\n# Make sure you don't have circular imports in your scripts!\n Possible solution: Replace @serializable(${offendingComponentName}) in your script with @serializable(Behaviour)\n\n---`, err)
868
868
  return;
869
869
  }
870
870
  if (!printedError) {
src/engine/engine_input.ts CHANGED
@@ -255,26 +255,7 @@
255
255
  /** Adds an event listener for the specified event type. The callback will be called when the event is triggered.
256
256
  * @param type The event type to listen for
257
257
  * @param callback The callback to call when the event is triggered
258
- * @param options The options for adding the event listener.
259
- * @example Basic usage:
260
- * ```ts
261
- * input.addEventListener("pointerdown", (evt) => {
262
- * console.log("Pointer down", evt.pointerId, evt.pointerType);
263
- * });
264
- * ```
265
- * @example Adding a listener that is called after all other listeners
266
- * By using a higher value for the queue the listener will be called after other listeners (default queue is 0).
267
- * ```ts
268
- * input.addEventListener("pointerdown", (evt) => {
269
- * console.log("Pointer down", evt.pointerId, evt.pointerType);
270
- * }, { queue: 10 });
271
- * ```
272
- * @example Adding a listener that is only called once
273
- * ```ts
274
- * input.addEventListener("pointerdown", (evt) => {
275
- * console.log("Pointer down", evt.pointerId, evt.pointerType);
276
- * }, { once: true });
277
- * ```
258
+ * @param options The options for adding the event listener
278
259
  */
279
260
  addEventListener(type: PointerEventNames, callback: PointerEventListener, options?: EventListenerOptions);
280
261
  addEventListener(type: KeyboardEventNames, callback: KeyboardEventListener, options?: EventListenerOptions);
src/engine/engine_mainloop_utils.ts CHANGED
@@ -326,7 +326,8 @@
326
326
  if (comp.enabled) {
327
327
  safeInvoke(comp.__internalAwake.bind(comp));
328
328
  if (comp.enabled) {
329
- comp.__internalEnable();
329
+ comp["__didEnable"] = true;
330
+ comp.onEnable();
330
331
  }
331
332
  }
332
333
  }
@@ -342,8 +343,9 @@
342
343
 
343
344
  let success = true;
344
345
  if (go.children) {
346
+ const nextLevel = level + 1;
345
347
  for (const ch of go.children) {
346
- const res = updateIsActiveInHierarchyRecursiveRuntime(ch, activeInHierarchy, allowEventCall, level + 1);
348
+ const res = updateIsActiveInHierarchyRecursiveRuntime(ch, activeInHierarchy, allowEventCall, nextLevel);
347
349
  if (res === false) success = false;
348
350
  }
349
351
  }
src/engine/engine_math.ts CHANGED
@@ -43,30 +43,16 @@
43
43
  return this.clamp(value, 0, 1);
44
44
  }
45
45
 
46
- /**
47
- * Linear interpolate
48
- */
49
46
  lerp(value1: number, value2: number, t: number) {
50
47
  t = t < 0 ? 0 : t;
51
48
  t = t > 1 ? 1 : t;
52
49
  return value1 + (value2 - value1) * t;
53
50
  }
54
51
 
55
- /**
56
- *
57
- */
58
52
  inverseLerp(value1: number, value2: number, t: number) {
59
53
  return (t - value1) / (value2 - value1);
60
54
  }
61
55
 
62
- /**
63
- * Remaps a value from one range to another.
64
- * @param value The value to remap.
65
- * @param min1 The minimum value of the current range.
66
- * @param max1 The maximum value of the current range.
67
- * @param min2 The minimum value of the target range.
68
- * @param max2 The maximum value of the target range.
69
- */
70
56
  remap(value: number, min1: number, max1: number, min2: number, max2: number) {
71
57
  return min2 + (max2 - min2) * (value - min1) / (max1 - min1);
72
58
  }
@@ -78,24 +64,20 @@
78
64
  return value1;
79
65
  }
80
66
 
81
-
82
-
83
- readonly Rad2Deg = 180 / Math.PI;
84
- readonly Deg2Rad = Math.PI / 180;
85
- readonly Epsilon = 0.00001;
86
- /**
87
- * Converts radians to degrees
88
- */
89
67
  toDegrees(radians: number) {
90
68
  return radians * 180 / Math.PI;
91
69
  }
92
- /**
93
- * Converts degrees to radians
94
- */
70
+
71
+ readonly Rad2Deg = 180 / Math.PI;
72
+
95
73
  toRadians(degrees: number) {
96
74
  return degrees * Math.PI / 180;
97
75
  }
98
76
 
77
+ readonly Deg2Rad = Math.PI / 180;
78
+
79
+ readonly Epsilon = 0.00001;
80
+
99
81
  tan(radians: number) {
100
82
  return Math.tan(radians);
101
83
  }
src/engine/engine_serialization_core.ts CHANGED
@@ -406,7 +406,7 @@
406
406
  }
407
407
  const hasOtherKeys = value !== undefined && Object.keys(value).length > 1;
408
408
  if (!hasOtherKeys) {
409
- addLog(LogType.Warn, `<strong>Missing serialization for object reference!</strong>\n\nPlease change to: \n@serializable(Object3D)\n${key}? : Object3D;\n\nin ${typeName}.ts\n<a href="https://docs.needle.tools/serializable" target="_blank">See documentation</a>`);
409
+ addLog(LogType.Warn, `<strong>Missing serialization for object reference!</strong>\n\nPlease change to: \n@serializable(Object3D)\n${key}? : Object3D;\n\nin script ${typeName}.ts\n<a href="https://docs.needle.tools/serializable" target="_blank">documentation</a>`);
410
410
  console.warn(typeName, key, obj[key], obj);
411
411
  continue;
412
412
  }
src/engine/engine_three_utils.ts CHANGED
@@ -398,7 +398,7 @@
398
398
  gl_FragColor = texture2D( map, uv);
399
399
  // gl_FragColor = vec4(uv.xy, 0, 1);
400
400
  }`;
401
- private static blitMaterial: ShaderMaterial | undefined = undefined;
401
+ private static blipMaterial: ShaderMaterial | undefined = undefined;
402
402
 
403
403
  /**
404
404
  * Create a blit material for copying textures
@@ -419,28 +419,27 @@
419
419
  * @returns the newly created, copied texture
420
420
  */
421
421
  static copyTexture(texture: Texture, blitMaterial?: ShaderMaterial): Texture {
422
+ const material = blitMaterial ?? this.blipMaterial!;
423
+
424
+ material.uniforms.map.value = texture;
425
+ material.needsUpdate = true;
426
+ material.uniformsNeedUpdate = true;
427
+
422
428
  // ensure that a blit material exists
423
- if (!this.blitMaterial) {
424
- this.blitMaterial = new ShaderMaterial({
429
+ if (!this.blipMaterial) {
430
+ this.blipMaterial = new ShaderMaterial({
425
431
  uniforms: { map: new Uniform(null) },
426
432
  vertexShader: this.vertex,
427
433
  fragmentShader: this.fragment
428
434
  });
429
435
  }
430
436
 
431
- const material = blitMaterial || this.blitMaterial;
432
-
433
- material.uniforms.map.value = texture;
434
- material.needsUpdate = true;
435
- material.uniformsNeedUpdate = true;
436
-
437
-
438
437
  // ensure that the blit material has the correct vertex shader
439
438
  const origVertexShader = material.vertexShader;
440
439
  material.vertexShader = this.vertex;
441
440
 
442
441
  if (!this.mesh)
443
- this.mesh = new Mesh(this.planeGeometry, this.blitMaterial);
442
+ this.mesh = new Mesh(this.planeGeometry, this.blipMaterial);
444
443
  const mesh = this.mesh;
445
444
  mesh.material = material;
446
445
  mesh.frustumCulled = false;
@@ -475,22 +474,15 @@
475
474
  // return new Texture(this.renderer.domElement);
476
475
  // }
477
476
 
478
- /**
479
- * Copy a texture to a HTMLCanvasElement
480
- * @param texture the texture convert
481
- * @param force if true the texture will be copied to a new texture before converting
482
- * @returns the HTMLCanvasElement with the texture or null if the texture could not be copied
483
- */
484
- static textureToCanvas(texture: Texture, force: boolean = false): HTMLCanvasElement | null {
485
- if (!texture) {
486
- return null;
487
- }
477
+ static textureToCanvas(texture: Texture, force: boolean) {
478
+ if (!texture) return null;
488
479
 
489
480
  if (force === true || texture["isCompressedTexture"] === true) {
490
481
  texture = copyTexture(texture);
491
482
  }
492
483
  const image = texture.image;
493
484
  if (isImageBitmap(image)) {
485
+
494
486
  const canvas = document.createElement('canvas');
495
487
  canvas.width = image.width;
496
488
  canvas.height = image.height;
@@ -502,8 +494,8 @@
502
494
  }
503
495
  context.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height);
504
496
  return canvas;
497
+
505
498
  }
506
-
507
499
  return null;
508
500
  }
509
501
  }
src/engine/engine_types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { QueryFilterFlags, World } from "@dimforge/rapier3d-compat";
1
+ import type { QueryFilterFlags } from "@dimforge/rapier3d-compat";
2
2
  import { AnimationClip, Color, Material, Mesh, Object3D, Quaternion } from "three";
3
3
  import { Vector3 } from "three";
4
4
  import { type GLTF as THREE_GLTF } from "three/examples/jsm/loaders/GLTFLoader.js";
@@ -442,241 +442,80 @@
442
442
 
443
443
 
444
444
  export interface IPhysicsEngine {
445
- /** Initializes the physics engine */
446
445
  initialize(): Promise<boolean>;
447
- /** Indicates whether the physics engine has been initialized */
448
446
  get isInitialized(): boolean;
449
- /** Advances physics simulation by the given time step */
450
447
  step(dt: number): void;
451
448
  postStep();
452
- /** Indicates whether the physics engine is currently updating */
453
449
  get isUpdating(): boolean;
454
- /** Clears all cached data (e.g., mesh data when creating scaled mesh colliders) */
450
+ /** clear all possibly cached data (e.g. mesh data when creating scaled mesh colliders) */
455
451
  clearCaches();
456
452
 
457
- /** Enables or disables the physics engine */
458
453
  enabled: boolean;
459
- /** Returns the underlying physics world object */
460
- get world(): World | undefined;
454
+ get world(): any;
461
455
 
462
- /** Sets the gravity vector for the physics simulation */
463
456
  set gravity(vec3: Vec3);
464
- /** Gets the current gravity vector */
465
457
  get gravity(): Vec3;
466
458
 
467
- /**
468
- * Gets the rapier physics body for a Needle component
469
- * @param obj The collider or rigidbody component
470
- * @returns The underlying physics body or null if not found
471
- */
459
+ /** Get the rapier body for a Needle component */
472
460
  getBody(obj: ICollider | IRigidbody): null | any;
473
- /**
474
- * Gets the Needle Engine component for a rapier physics object
475
- * @param rapierObject The rapier physics object
476
- * @returns The associated component or null if not found
477
- */
461
+ /** Get the Needle Engine component for a rapier object */
478
462
  getComponent(rapierObject: object): IComponent | null;
479
463
 
480
- /**
481
- * Performs a fast raycast against physics colliders
482
- * @param origin Ray origin in screen or worldspace
483
- * @param direction Ray direction in worldspace
484
- * @param options Additional raycast configuration options
485
- * @returns Raycast result containing hit point and collider, or null if no hit
464
+ // raycasting
465
+ /** Fast raycast against physics colliders
466
+ * @param origin ray origin in screen or worldspace
467
+ * @param direction ray direction in worldspace
468
+ * @param options additional options
486
469
  */
487
470
  raycast(origin?: Vec2 | Vec3, direction?: Vec3, options?: {
488
471
  maxDistance?: number,
489
472
  /** True if you want to also hit objects when the raycast starts from inside a collider */
490
473
  solid?: boolean,
491
474
  queryFilterFlags?: QueryFilterFlags,
492
- /**
493
- * Raycast filter groups. Groups are used to apply the collision group rules for the scene query.
494
- * The scene query will only consider hits with colliders with collision groups compatible with
495
- * this collision group (using the bitwise test described in the collision groups section).
496
- * For example membership 0x0001 and filter 0x0002 should be 0x00010002
497
- * @see https://rapier.rs/docs/user_guides/javascript/colliders#collision-groups-and-solver-groups
498
- */
475
+ /** Raycast filter groups. Groups are used to apply the collision group rules for the scene query. The scene query will only consider hits with colliders with collision groups compatible with this collision group (using the bitwise test described in the collision groups section). See https://rapier.rs/docs/user_guides/javascript/colliders#collision-groups-and-solver-groups
476
+ * For example membership 0x0001 and filter 0x0002 should be 0x00010002 */
499
477
  filterGroups?: number,
500
- /**
501
- * Predicate to filter colliders in raycast results
502
- * @param collider The collider being tested
503
- * @returns False to ignore this collider, true to include it
504
- */
478
+ /** Return false to ignore this collider */
505
479
  filterPredicate?: (collider: ICollider) => boolean
506
480
  }): RaycastResult;
507
-
508
- /**
509
- * Performs a raycast that also returns the normal vector at the hit point
510
- * @param origin Ray origin in screen or worldspace
511
- * @param direction Ray direction in worldspace
512
- * @param options Additional raycast configuration options
513
- * @returns Raycast result containing hit point, normal, and collider, or null if no hit
514
- */
481
+ /** raycast that also gets the normal vector. If you don't need it use raycast() */
515
482
  raycastAndGetNormal(origin?: Vec2 | Vec3, direction?: Vec3, options?: {
516
483
  maxDistance?: number,
517
484
  /** True if you want to also hit objects when the raycast starts from inside a collider */
518
485
  solid?: boolean,
519
486
  queryFilterFlags?: QueryFilterFlags,
520
- /**
521
- * Raycast filter groups. Groups are used to apply the collision group rules for the scene query.
522
- * The scene query will only consider hits with colliders with collision groups compatible with
523
- * this collision group (using the bitwise test described in the collision groups section).
524
- * For example membership 0x0001 and filter 0x0002 should be 0x00010002
525
- * @see https://rapier.rs/docs/user_guides/javascript/colliders#collision-groups-and-solver-groups
526
- */
487
+ /** Raycast filter groups. Groups are used to apply the collision group rules for the scene query. The scene query will only consider hits with colliders with collision groups compatible with this collision group (using the bitwise test described in the collision groups section). See https://rapier.rs/docs/user_guides/javascript/colliders#collision-groups-and-solver-groups
488
+ * For example membership 0x0001 and filter 0x0002 should be 0x00010002 */
527
489
  filterGroups?: number,
528
- /**
529
- * Predicate to filter colliders in raycast results
530
- * @param collider The collider being tested
531
- * @returns False to ignore this collider, true to include it
532
- */
490
+ /** Return false to ignore this collider */
533
491
  filterPredicate?: (collider: ICollider) => boolean
534
492
  }): RaycastResult;
535
-
536
- /**
537
- * Finds all colliders within a sphere
538
- * @param point The center point of the sphere
539
- * @param radius The radius of the sphere
540
- * @returns Array of objects that overlap with the sphere
541
- */
542
493
  sphereOverlap(point: Vector3, radius: number): Array<SphereOverlapResult>;
543
494
 
544
495
  // Collider methods
545
- /**
546
- * Adds a sphere collider to the physics world
547
- * @param collider The collider component to add
548
- */
549
496
  addSphereCollider(collider: ICollider);
550
-
551
- /**
552
- * Adds a box collider to the physics world
553
- * @param collider The collider component to add
554
- * @param size The size of the box
555
- */
556
497
  addBoxCollider(collider: ICollider, size: Vector3);
557
-
558
- /**
559
- * Adds a capsule collider to the physics world
560
- * @param collider The collider component to add
561
- * @param radius The radius of the capsule
562
- * @param height The height of the capsule
563
- */
564
498
  addCapsuleCollider(collider: ICollider, radius: number, height: number);
565
-
566
- /**
567
- * Adds a mesh collider to the physics world
568
- * @param collider The collider component to add
569
- * @param mesh The mesh to use for collision
570
- * @param convex Whether the collision mesh should be treated as convex
571
- * @param scale Optional scale to apply to the mesh
572
- */
573
499
  addMeshCollider(collider: ICollider, mesh: Mesh, convex: boolean, scale?: Vector3 | undefined);
574
500
 
575
- /**
576
- * Updates the physics material properties of a collider
577
- * @param collider The collider to update
578
- */
579
501
  updatePhysicsMaterial(collider: ICollider);
580
502
 
581
503
  // Rigidbody methods
582
- /**
583
- * Wakes up a sleeping rigidbody
584
- * @param rb The rigidbody to wake up
585
- */
586
504
  wakeup(rb: IRigidbody);
587
-
588
- /**
589
- * Checks if a rigidbody is currently sleeping
590
- * @param rb The rigidbody to check
591
- * @returns Whether the rigidbody is sleeping or undefined if cannot be determined
592
- */
593
505
  isSleeping(rb: IRigidbody): boolean | undefined;
594
-
595
- /**
596
- * Updates the physical properties of a rigidbody or collider
597
- * @param rb The rigidbody or collider to update
598
- */
599
506
  updateProperties(rb: IRigidbody | ICollider);
600
-
601
- /**
602
- * Resets all forces acting on a rigidbody
603
- * @param rb The rigidbody to reset forces on
604
- * @param wakeup Whether to wake up the rigidbody
605
- */
606
507
  resetForces(rb: IRigidbody, wakeup: boolean);
607
-
608
- /**
609
- * Resets all torques acting on a rigidbody
610
- * @param rb The rigidbody to reset torques on
611
- * @param wakeup Whether to wake up the rigidbody
612
- */
613
508
  resetTorques(rb: IRigidbody, wakeup: boolean);
614
-
615
- /**
616
- * Adds a continuous force to a rigidbody
617
- * @param rb The rigidbody to add force to
618
- * @param vec The force vector to add
619
- * @param wakeup Whether to wake up the rigidbody
620
- */
621
509
  addForce(rb: IRigidbody, vec: Vec3, wakeup: boolean);
622
-
623
- /**
624
- * Applies an instantaneous impulse to a rigidbody
625
- * @param rb The rigidbody to apply impulse to
626
- * @param vec The impulse vector to apply
627
- * @param wakeup Whether to wake up the rigidbody
628
- */
629
510
  applyImpulse(rb: IRigidbody, vec: Vec3, wakeup: boolean);
630
-
631
- /**
632
- * Gets the linear velocity of a rigidbody or the rigidbody attached to a collider
633
- * @param rb The rigidbody or collider to get velocity from
634
- * @returns The linear velocity vector or null if not available
635
- */
511
+ /** Returns the linear velocity of a rigidbody or the rigidbody of a collider */
636
512
  getLinearVelocity(rb: IRigidbody | ICollider): Vec3 | null;
637
-
638
- /**
639
- * Gets the angular velocity of a rigidbody
640
- * @param rb The rigidbody to get angular velocity from
641
- * @returns The angular velocity vector or null if not available
642
- */
643
513
  getAngularVelocity(rb: IRigidbody): Vec3 | null;
644
-
645
- /**
646
- * Sets the angular velocity of a rigidbody
647
- * @param rb The rigidbody to set angular velocity for
648
- * @param vec The angular velocity vector to set
649
- * @param wakeup Whether to wake up the rigidbody
650
- */
651
514
  setAngularVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean);
652
-
653
- /**
654
- * Sets the linear velocity of a rigidbody
655
- * @param rb The rigidbody to set linear velocity for
656
- * @param vec The linear velocity vector to set
657
- * @param wakeup Whether to wake up the rigidbody
658
- */
659
515
  setLinearVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean);
660
516
 
661
- /**
662
- * Updates the position and/or rotation of a physics body
663
- * @param comp The collider or rigidbody component to update
664
- * @param translation Whether to update the position
665
- * @param rotation Whether to update the rotation
666
- */
667
517
  updateBody(comp: ICollider | IRigidbody, translation: boolean, rotation: boolean);
668
-
669
- /**
670
- * Removes a physics body from the simulation
671
- * @param body The component whose physics body should be removed
672
- */
673
518
  removeBody(body: IComponent);
674
-
675
- /**
676
- * Gets the physics body for a component
677
- * @param obj The collider or rigidbody component
678
- * @returns The underlying physics body or null if not found
679
- */
680
519
  getBody(obj: ICollider | IRigidbody): null | any;
681
520
 
682
521
  // Joints
src/engine/engine_utils_screenshot.ts CHANGED
@@ -10,7 +10,6 @@
10
10
  import { registerFrameEventCallback } from "./engine_lifecycle_functions_internal.js";
11
11
  import { Context, FrameEvent } from "./engine_setup.js";
12
12
  import { ICamera } from "./engine_types.js";
13
- import { DeviceUtilities } from "./engine_utils.js";
14
13
  import { updateTextureFromXRFrame } from "./engine_utils_screenshot.xr.js";
15
14
  import { RGBAColor } from "./js-extensions/index.js";
16
15
  import { setCustomVisibility } from "./js-extensions/Layers.js";
@@ -396,22 +395,8 @@
396
395
 
397
396
  if ("download_filename" in opts && opts.download_filename) {
398
397
  let download_name = opts.download_filename;
399
- // On mobile we don't want to see the dialogue for every screenshot
400
- if (DeviceUtilities.isMobileDevice() && typeof window !== "undefined") {
401
- const key = download_name + "_screenshots";
402
- const parts = download_name.split(".");
403
- const ext = parts.pop()?.toLowerCase();
404
- let count = 0;
405
- if (localStorage.getItem(key)) {
406
- count = parseInt(sessionStorage.getItem(key) || "0");
407
- }
408
- if (count > 0) {
409
- // const timestamp = new Date().toLocaleString();
410
- download_name = `${parts.join()}-${count}.${ext}`;
411
- }
412
- count += 1;
413
- sessionStorage.setItem(key, count.toString());
414
- }
398
+ const ext = download_name.split(".").pop()?.toLowerCase();
399
+ download_name = `${download_name}-${Date.now()}.${ext}`;
415
400
  saveImage(dataUrl, download_name);
416
401
  }
417
402
  return dataUrl;
@@ -439,6 +424,7 @@
439
424
 
440
425
 
441
426
 
427
+
442
428
  // trim to transparent pixels
443
429
  function trimCanvas(originalCanvas: HTMLCanvasElement): HTMLCanvasElement | null {
444
430
  if (!("document" in globalThis)) return null;
src/engine-components/ui/EventSystem.ts CHANGED
@@ -59,7 +59,6 @@
59
59
  return ctx.scene.getComponent(EventSystem);
60
60
  }
61
61
 
62
- /** Get the currently active event system */
63
62
  static get instance(): EventSystem | null {
64
63
  return this.get(Context.Current);
65
64
  }
@@ -76,10 +75,7 @@
76
75
  }
77
76
  }
78
77
 
79
- get hasActiveUI() {
80
- return this.currentActiveMeshUIComponents.length > 0;
81
- }
82
-
78
+ get hasActiveUI() { return this.currentActiveMeshUIComponents.length > 0; }
83
79
  get isHoveringObjects() { return this.hoveredByID.size > 0; }
84
80
 
85
81
  awake(): void {
@@ -428,6 +424,7 @@
428
424
  const res = UIRaycastUtils.isInteractable(actualGo, this.out);
429
425
  if (!res) return false;
430
426
  canvasGroup = this.out.canvasGroup ?? null;
427
+
431
428
  const handled = this.handleMeshUIIntersection(object, pressedOrClicked);
432
429
  if (!clicked && handled) {
433
430
  // return true;
@@ -756,6 +753,7 @@
756
753
  }
757
754
 
758
755
  private resetMeshUIStates() {
756
+
759
757
  if (this.context.input.getPointerPressedCount() > 0) {
760
758
  MeshUIHelper.resetLastSelected();
761
759
  }
@@ -818,7 +816,7 @@
818
816
  let foundBlock: Object3D | null = null;
819
817
 
820
818
  if (intersect) {
821
- foundBlock = this.findBlockOrTextInParent(intersect);
819
+ foundBlock = this.findBlockInParent(intersect);
822
820
  // console.log(intersect, "-- found block:", foundBlock)
823
821
  if (foundBlock && foundBlock !== this.lastSelected) {
824
822
  const interactable = foundBlock["interactable"];
@@ -842,13 +840,13 @@
842
840
  this.needsUpdate = true;
843
841
  }
844
842
 
845
- static findBlockOrTextInParent(elem: any): Object3D | null {
843
+ static findBlockInParent(elem: any): Object3D | null {
846
844
  if (!elem) return null;
847
- if (elem.isBlock || (elem.isText)) {
845
+ if (elem.isBlock) {
848
846
  // @TODO : Replace states managements
849
847
  // if (Object.keys(elem.states).length > 0)
850
848
  return elem;
851
849
  }
852
- return this.findBlockOrTextInParent(elem.parent);
850
+ return this.findBlockInParent(elem.parent);
853
851
  }
854
852
  }
src/engine-components/Light.ts CHANGED
@@ -24,86 +24,82 @@
24
24
  const debug = getParam("debuglights");
25
25
 
26
26
 
27
- /**
28
- * Defines the type of light in a scene.
29
- */
27
+ /// <summary>
28
+ /// <para>The type of a Light.</para>
29
+ /// </summary>
30
30
  export enum LightType {
31
- /** Spot light that emits light in a cone shape */
31
+ /// <summary>
32
+ /// <para>The light is a spot light.</para>
33
+ /// </summary>
32
34
  Spot = 0,
33
- /** Directional light that emits parallel light rays in a specific direction */
35
+ /// <summary>
36
+ /// <para>The light is a directional light.</para>
37
+ /// </summary>
34
38
  Directional = 1,
35
- /** Point light that emits light in all directions from a single point */
39
+ /// <summary>
40
+ /// <para>The light is a point light.</para>
41
+ /// </summary>
36
42
  Point = 2,
37
- /** Area light */
38
43
  Area = 3,
39
- /** Rectangle shaped area light that only affects baked lightmaps and light probes */
44
+ /// <summary>
45
+ /// <para>The light is a rectangle shaped area light. It affects only baked lightmaps and lightprobes.</para>
46
+ /// </summary>
40
47
  Rectangle = 3,
41
- /** Disc shaped area light that only affects baked lightmaps and light probes */
48
+ /// <summary>
49
+ /// <para>The light is a disc shaped area light. It affects only baked lightmaps and lightprobes.</para>
50
+ /// </summary>
42
51
  Disc = 4,
43
52
  }
44
-
45
- /**
46
- * Defines how a light contributes to the scene lighting.
47
- */
48
53
  export enum LightmapBakeType {
49
- /** Light affects the scene in real-time with no baking */
54
+ /// <summary>
55
+ /// <para>Realtime lights cast run time light and shadows. They can change position, orientation, color, brightness, and many other properties at run time. No lighting gets baked into lightmaps or light probes..</para>
56
+ /// </summary>
50
57
  Realtime = 4,
51
- /** Light is completely baked into lightmaps and light probes */
58
+ /// <summary>
59
+ /// <para>Baked lights cannot move or change in any way during run time. All lighting for static objects gets baked into lightmaps. Lighting and shadows for dynamic objects gets baked into Light Probes.</para>
60
+ /// </summary>
52
61
  Baked = 2,
53
- /** Combines aspects of realtime and baked lighting */
62
+ /// <summary>
63
+ /// <para>Mixed lights allow a mix of realtime and baked lighting, based on the Mixed Lighting Mode used. These lights cannot move, but can change color and intensity at run time. Changes to color and intensity only affect direct lighting as indirect lighting gets baked. If using Subtractive mode, changes to color or intensity are not calculated at run time on static objects.</para>
64
+ /// </summary>
54
65
  Mixed = 1,
55
66
  }
56
67
 
57
- /**
58
- * Defines the shadow casting options for a Light.
59
- * @enum {number}
60
- */
68
+ /// <summary>
69
+ /// <para>Shadow casting options for a Light.</para>
70
+ /// </summary>
61
71
  enum LightShadows {
62
- /** No shadows are cast */
72
+ /// <summary>
73
+ /// <para>Do not cast shadows (default).</para>
74
+ /// </summary>
63
75
  None = 0,
64
- /** Hard-edged shadows without filtering */
76
+ /// <summary>
77
+ /// <para>Cast "hard" shadows (with no shadow filtering).</para>
78
+ /// </summary>
65
79
  Hard = 1,
66
- /** Soft shadows with PCF filtering */
80
+ /// <summary>
81
+ /// <para>Cast "soft" shadows (with 4x PCF filtering).</para>
82
+ /// </summary>
67
83
  Soft = 2,
68
84
  }
69
85
 
70
- /**
71
- * The Light component creates a light source in the scene.
72
- * Supports directional, spot, and point light types with various customization options.
73
- * Lights can cast shadows with configurable settings and can be set to baked or realtime rendering.
74
- *
75
- * Debug mode can be enabled with the URL parameter `?debuglights`, which shows
76
- * additional console output and visual helpers for lights.
77
- *
86
+ /** The Light component can be used to create a light source in the scene.
87
+ * It can be used to create a directional light, a spot light or a point light.
88
+ * The light can be set to cast shadows and the shadow type can be set to hard or soft shadows.
89
+ * The light can be set to be baked or realtime.
90
+ * The light can be set to be a main light which will be used for the main directional light in the scene.
78
91
  * @category Rendering
79
92
  * @group Components
80
93
  */
81
94
  export class Light extends Behaviour implements ILight {
82
95
 
83
- /**
84
- * The type of light (spot, directional, point, etc.)
85
- */
86
96
  @serializable()
87
97
  private type: LightType = 0;
88
98
 
89
- /**
90
- * The maximum distance the light affects
91
- */
92
99
  public range: number = 1;
93
-
94
- /**
95
- * The full outer angle of the spotlight cone in degrees
96
- */
97
100
  public spotAngle: number = 1;
98
-
99
- /**
100
- * The angle of the inner cone in degrees for soft-edge spotlights
101
- */
102
101
  public innerSpotAngle: number = 1;
103
102
 
104
- /**
105
- * The color of the light
106
- */
107
103
  @serializable(Color)
108
104
  set color(val: Color) {
109
105
  this._color = val;
@@ -117,9 +113,6 @@
117
113
  }
118
114
  public _color: Color = new Color(0xffffff);
119
115
 
120
- /**
121
- * The near plane distance for shadow projection
122
- */
123
116
  @serializable()
124
117
  set shadowNearPlane(val: number) {
125
118
  if (val === this._shadowNearPlane) return;
@@ -132,9 +125,6 @@
132
125
  get shadowNearPlane(): number { return this._shadowNearPlane; }
133
126
  private _shadowNearPlane: number = .1;
134
127
 
135
- /**
136
- * Shadow bias value to reduce shadow acne and peter-panning
137
- */
138
128
  @serializable()
139
129
  set shadowBias(val: number) {
140
130
  if (val === this._shadowBias) return;
@@ -147,9 +137,6 @@
147
137
  get shadowBias(): number { return this._shadowBias; }
148
138
  private _shadowBias: number = 0;
149
139
 
150
- /**
151
- * Shadow normal bias to reduce shadow acne on sloped surfaces
152
- */
153
140
  @serializable()
154
141
  set shadowNormalBias(val: number) {
155
142
  if (val === this._shadowNormalBias) return;
@@ -165,9 +152,6 @@
165
152
  /** when enabled this will remove the multiplication when setting the shadow bias settings initially */
166
153
  private _overrideShadowBiasSettings: boolean = false;
167
154
 
168
- /**
169
- * Shadow casting mode (None, Hard, or Soft)
170
- */
171
155
  @serializable()
172
156
  set shadows(val: LightShadows) {
173
157
  this._shadows = val;
@@ -179,16 +163,9 @@
179
163
  get shadows(): LightShadows { return this._shadows; }
180
164
  private _shadows: LightShadows = 1;
181
165
 
182
- /**
183
- * Determines if the light contributes to realtime lighting, baked lighting, or a mix
184
- */
185
166
  @serializable()
186
167
  private lightmapBakeType: LightmapBakeType = LightmapBakeType.Realtime;
187
168
 
188
- /**
189
- * Brightness of the light. In WebXR experiences, the intensity is automatically
190
- * adjusted based on the AR session scale to maintain consistent lighting.
191
- */
192
169
  @serializable()
193
170
  set intensity(val: number) {
194
171
  this._intensity = val;
@@ -207,9 +184,6 @@
207
184
  get intensity(): number { return this._intensity; }
208
185
  private _intensity: number = -1;
209
186
 
210
- /**
211
- * Maximum distance the shadow is projected
212
- */
213
187
  @serializable()
214
188
  get shadowDistance(): number {
215
189
  const light = this.light;
@@ -233,9 +207,6 @@
233
207
  private shadowWidth?: number;
234
208
  private shadowHeight?: number;
235
209
 
236
- /**
237
- * Resolution of the shadow map in pixels (width and height)
238
- */
239
210
  @serializable()
240
211
  get shadowResolution(): number {
241
212
  const light = this.light;
@@ -255,16 +226,10 @@
255
226
  }
256
227
  private _shadowResolution?: number = undefined;
257
228
 
258
- /**
259
- * Whether this light's illumination is entirely baked into lightmaps
260
- */
261
229
  get isBaked() {
262
230
  return this.lightmapBakeType === LightmapBakeType.Baked;
263
231
  }
264
232
 
265
- /**
266
- * Checks if the GameObject itself is a {@link ThreeLight} object
267
- */
268
233
  private get selfIsLight(): boolean {
269
234
  if (this.gameObject["isLight"] === true) return true;
270
235
  switch (this.gameObject.type) {
@@ -276,16 +241,8 @@
276
241
  return false;
277
242
  }
278
243
 
279
- /**
280
- * The underlying three.js {@link ThreeLight} instance
281
- */
282
244
  private light: ThreeLight | undefined = undefined;
283
245
 
284
- /**
285
- * Gets the world position of the light
286
- * @param vec Vector3 to store the result
287
- * @returns The world position as a Vector3
288
- */
289
246
  public getWorldPosition(vec: Vector3): Vector3 {
290
247
  if (this.light) {
291
248
  if (this.type === LightType.Directional) {
@@ -354,10 +311,6 @@
354
311
  // this.updateIntensity();
355
312
  }
356
313
 
357
- /**
358
- * Creates the appropriate three.js light based on the configured light type
359
- * and applies all settings like shadows, intensity, and color.
360
- */
361
314
  createLight() {
362
315
  const lightAlreadyCreated = this.selfIsLight;
363
316
 
@@ -494,10 +447,6 @@
494
447
 
495
448
  }
496
449
 
497
- /**
498
- * Coroutine that updates the main light reference in the context
499
- * if this directional light should be the main light
500
- */
501
450
  *updateMainLightRoutine() {
502
451
  while (true) {
503
452
  if (this.type === LightType.Directional) {
@@ -510,14 +459,8 @@
510
459
  }
511
460
  }
512
461
 
513
- /**
514
- * Controls whether the renderer's shadow map type can be changed when soft shadows are used
515
- */
516
462
  static allowChangingRendererShadowMapType: boolean = true;
517
463
 
518
- /**
519
- * Updates shadow settings based on whether the shadows are set to hard or soft
520
- */
521
464
  private updateShadowSoftHard() {
522
465
  if (!this.light) return;
523
466
  if (!this.light.shadow) return;
@@ -543,10 +486,6 @@
543
486
  }
544
487
  }
545
488
 
546
- /**
547
- * Configures a directional light by adding and positioning its target
548
- * @param dirLight The directional light to set up
549
- */
550
489
  private setDirectionalLight(dirLight: DirectionalLight) {
551
490
  dirLight.add(dirLight.target);
552
491
  dirLight.target.position.set(0, 0, -1);
src/engine-components/NeedleMenu.ts CHANGED
@@ -4,68 +4,50 @@
4
4
  import { Behaviour } from './Component.js';
5
5
 
6
6
  /**
7
- * Provides configuration options for the built-in Needle Menu.
8
- * Needle Menu uses HTML in 2D mode, and automatically switches to a 3D menu in VR/AR mode.
9
- *
10
- * Controls display options, button visibility, and menu positioning.
7
+ * Exposes options to customize the built-in Needle Menu.
11
8
  * From code, you can access the menu via {@link Context.menu}.
12
9
  * @category User Interface
13
10
  * @group Components
14
11
  **/
15
12
  export class NeedleMenu extends Behaviour {
16
13
 
17
- /**
18
- * Determines the vertical positioning of the menu on the screen
19
- */
20
14
  @serializable()
21
15
  position: "top" | "bottom" = "bottom";
22
16
 
23
- /**
24
- * Controls the visibility of the Needle logo in the menu (requires PRO license)
25
- */
17
+ /** Show the Needle logo in the menu (requires PRO license) */
26
18
  @serializable()
27
19
  showNeedleLogo: boolean = true;
28
20
 
29
- /**
30
- * When enabled, displays the menu in VR/AR mode when the user looks up
21
+ /** When enabled the menu will also be visible in VR/AR when you look up
31
22
  * @default undefined
32
- */
23
+ */
33
24
  @serializable()
34
25
  showSpatialMenu?: boolean;
35
26
 
36
- /**
37
- * When enabled, adds a fullscreen toggle button to the menu
27
+ /** When enabled a button to enter fullscreen will be added to the menu
38
28
  * @default undefined
39
- */
29
+ */
40
30
  @serializable()
41
31
  createFullscreenButton?: boolean;
42
-
43
- /**
44
- * When enabled, adds an audio mute/unmute button to the menu
32
+ /** When enabled a button to mute the application will be added to the menu
45
33
  * @default undefined
46
- */
34
+ */
47
35
  @serializable()
48
36
  createMuteButton?: boolean;
49
37
 
50
38
  /**
51
- * When enabled, adds a button to display a QR code for sharing the application.
52
- * The QR code is only displayed on desktop devices.
39
+ * When enabled a button to show a QR code will be added to the menu.
53
40
  * @default undefined
54
41
  */
55
42
  @serializable()
56
43
  createQRCodeButton?: boolean;
57
44
 
58
- /**
59
- * Applies the configured menu options when the component is enabled
60
- * @hidden
61
- */
45
+ /** @hidden */
62
46
  onEnable() {
63
47
  this.applyOptions();
64
48
  }
65
49
 
66
- /**
67
- * Applies all configured options to the active {@link Context.menu}.
68
- */
50
+ /** applies the options to `this.context.menu` */
69
51
  applyOptions() {
70
52
  this.context.menu.setPosition(this.position);
71
53
  this.context.menu.showNeedleLogo(this.showNeedleLogo);
src/engine/xr/NeedleXRSession.ts CHANGED
@@ -693,13 +693,7 @@
693
693
  }
694
694
  private _rigScale: number = 1;
695
695
  private _lastRigScaleUpdate: number = -1;
696
-
697
- /** Get the XR Rig worldscale.
698
- *
699
- * **For AR**
700
- * If you want to modify the scale in AR at runtime get the WebARSessionRoot component via `findObjectOfType(WebARSessionRoot)` and then set the `arScale` value.
701
- *
702
- */
696
+ /** get the XR rig worldscale */
703
697
  get rigScale() {
704
698
  if (!this._rigs[0]) return 1;
705
699
  if (this._lastRigScaleUpdate !== this.context.time.frame) {
src/engine-components/Networking.ts CHANGED
@@ -7,33 +7,21 @@
7
7
  const debug = getParam("debugnet");
8
8
 
9
9
  /**
10
- * Provides configuration to the built-in networking system.
11
- * This component supplies websocket URLs for establishing connections.
12
- * It implements the {@link INetworkingWebsocketUrlProvider} interface.
13
- *
10
+ * The networking component is used to provide a websocket url to the networking system. It implements the {@link INetworkingWebsocketUrlProvider} interface.
14
11
  * @category Networking
15
12
  * @group Components
16
13
  */
17
14
  export class Networking extends Behaviour implements INetworkingWebsocketUrlProvider {
18
15
 
19
- /**
20
- * The websocket URL to connect to for networking functionality.
21
- * Can be a complete URL or a relative path that will be resolved against the current origin.
22
- */
16
+ /** The url that should be used for the websocket connection */
23
17
  @serializable()
24
18
  url: string | null = null;
25
19
 
26
- /**
27
- * Name of the URL parameter that can override the websocket connection URL.
28
- * When set, the URL will be overridden by the parameter value from the browser URL.
29
- * For example, with `urlParameterName="ws"`, adding `?ws=ws://localhost:8080` to the browser URL will override the connection URL.
30
- */
20
+ /** The name of the url parameter that should be used to override the url. When set the url will be overridden by the url parameter e.g. when `urlParameterName=ws` `?ws=ws://localhost:8080` */
31
21
  @serializable()
32
22
  urlParameterName: string | null = null;
33
23
 
34
- /**
35
- * Alternative URL to use when running on a local network.
36
- * This is particularly useful for development, when the server is running on the same machine as the client.
24
+ /** Thie localhost url that should be used when the networking is running on a local network. This is useful when the server is running on the same machine as the client.
37
25
  */
38
26
  @serializable()
39
27
  localhost: string | null = null;
@@ -45,13 +33,7 @@
45
33
  this.context.connection.registerProvider(this);
46
34
  }
47
35
 
48
- /**
49
- * Determines the websocket URL to use for networking connections.
50
- * Processes the configured URL, applying localhost fallbacks when appropriate and
51
- * handling URL parameter overrides if specified.
52
- * @returns The formatted websocket URL string or null if no valid URL could be determined
53
- * @internal
54
- */
36
+ /** @internal */
55
37
  getWebsocketUrl(): string | null {
56
38
 
57
39
  let socketurl = this.url ? Networking.GetUrl(this.url, this.localhost) : null;
@@ -76,13 +58,7 @@
76
58
  return "wss://" + match?.groups["url"];
77
59
  }
78
60
 
79
- /**
80
- * Processes a URL string applying various transformations based on network environment.
81
- * Handles relative paths and localhost fallbacks for local network environments.
82
- * @param url The original URL to process
83
- * @param localhostFallback Alternative URL to use when on a local network
84
- * @returns The processed URL string or null/undefined if input was invalid
85
- */
61
+
86
62
  public static GetUrl(url: string | null | undefined, localhostFallback?: string | null): string | null | undefined {
87
63
 
88
64
  let result = url;
@@ -102,13 +78,6 @@
102
78
  return result;
103
79
  }
104
80
 
105
- /**
106
- * Determines if the current connection is on a local network.
107
- * Useful for applying different networking configurations in local development environments.
108
- * This is the same as calling {@link isLocalNetwork}.
109
- * @param hostname Optional hostname to check instead of the current window location
110
- * @returns True if the connection is on a local network, false otherwise
111
- */
112
81
  public static IsLocalNetwork(hostname = window.location.hostname) {
113
82
  return isLocalNetwork(hostname);
114
83
  }
src/engine-components/particlesystem/ParticleSystem.ts CHANGED
@@ -94,7 +94,7 @@
94
94
  else this.particleMaterial = newMaterial;
95
95
  }
96
96
 
97
-
97
+
98
98
  if (material.map) {
99
99
  material.map.colorSpace = LinearSRGBColorSpace;
100
100
  material.map.premultiplyAlpha = false;
@@ -916,7 +916,7 @@
916
916
  if (particleSystemBehaviour instanceof ParticleSystemBaseBehaviour) {
917
917
  particleSystemBehaviour.system = this;
918
918
  }
919
- if (debug) console.debug("Add custom ParticleSystem Behaviour", particleSystemBehaviour);
919
+ if (isDevEnvironment() || debug) console.debug("Add custom ParticleSystem Behaviour", particleSystemBehaviour);
920
920
  this._particleSystem.addBehavior(particleSystemBehaviour);
921
921
  return true;
922
922
  }
src/engine-components-experimental/networking/PlayerSync.ts CHANGED
@@ -12,7 +12,6 @@
12
12
 
13
13
  const debug = getParam("debugplayersync");
14
14
 
15
- /** Type definition for a PlayerSync instance with a guaranteed asset property */
16
15
  declare type PlayerSyncWithAsset = PlayerSync & Required<Pick<PlayerSync, "asset">>;
17
16
 
18
17
  /**
@@ -24,10 +23,7 @@
24
23
 
25
24
  /**
26
25
  * This API is experimental and may change or be removed in the future.
27
- * Creates a PlayerSync instance at runtime from a given URL and sets it up for networking
28
- * @param url Path to the asset that should be instantiated for each player
29
- * @param init Optional initialization parameters for the PlayerSync component
30
- * @returns Promise resolving to a PlayerSync instance with a guaranteed asset property
26
+ * Create a PlayerSync instance at runtime from a given URL
31
27
  * @example
32
28
  * ```typescript
33
29
  * const res = await PlayerSync.setupFrom("/assets/demo.glb");
@@ -51,22 +47,15 @@
51
47
  return ps as PlayerSyncWithAsset;
52
48
  }
53
49
 
54
- /**
55
- * When enabled, PlayerSync will automatically load and instantiate the assigned asset when joining a networked room
56
- */
50
+ /** when enabled PlayerSync will automatically load and instantiate the assigned asset when joining a networked room */
57
51
  @serializable()
58
52
  autoSync: boolean = true;
59
53
 
60
- /**
61
- * The asset that will be loaded and instantiated when PlayerSync becomes active and joins a networked room
62
- */
54
+ /** This asset will be loaded and instantiated when PlayerSync becomes active and joins a networked room */
63
55
  @serializable(AssetReference)
64
56
  asset?: AssetReference;
65
57
 
66
- /**
67
- * Event invoked when a player instance is spawned with the spawned {@link Object3D} as parameter
68
- * @serializable
69
- */
58
+ /** Event called when an instance is spawned */
70
59
  @serializable(EventList)
71
60
  onPlayerSpawned?: EventList<Object3D>;
72
61
 
@@ -97,10 +86,6 @@
97
86
  if (this.autoSync) this.getInstance();
98
87
  }
99
88
 
100
- /**
101
- * Gets or creates an instance of the assigned asset for the local player
102
- * @returns Promise resolving to the instantiated player object or null if creation failed
103
- */
104
89
  async getInstance() {
105
90
  if (this._localInstance) return this._localInstance;
106
91
 
@@ -138,9 +123,6 @@
138
123
  return this._localInstance;
139
124
  }
140
125
 
141
- /**
142
- * Destroys the current player instance and cleans up networking state
143
- */
144
126
  destroyInstance = () => {
145
127
  this._localInstance?.then(go => {
146
128
  if (debug) console.log("PlayerSync.destroyInstance", go);
@@ -149,9 +131,8 @@
149
131
  this._localInstance = undefined;
150
132
  }
151
133
 
152
- /**
153
- * Sets up visibility change listeners to handle player cleanup when browser tab visibility changes
154
- */
134
+
135
+
155
136
  private watchTabVisible() {
156
137
  window.addEventListener("visibilitychange", _ => {
157
138
  if (document.visibilityState === "visible") {
@@ -166,50 +147,32 @@
166
147
  }
167
148
  }
168
149
 
169
- /**
170
- * Enum defining events that can be triggered by PlayerState
171
- */
172
150
  export enum PlayerStateEvent {
173
- /** Triggered when a PlayerState's owner property changes */
174
151
  OwnerChanged = "ownerChanged",
175
152
  }
176
153
 
177
- /** Arguments passed when a PlayerState's owner changes */
178
154
  export declare interface PlayerStateOwnerChangedArgs {
179
- /** The PlayerState instance that changed */
180
155
  playerState: PlayerState;
181
- /** Previous owner's connection ID */
182
156
  oldValue: string;
183
- /** New owner's connection ID */
184
157
  newValue: string;
185
158
  }
186
159
 
187
- /** Callback type for PlayerState events */
188
160
  export declare type PlayerStateEventCallback = (args: CustomEvent<PlayerStateOwnerChangedArgs>) => void;
189
161
 
190
- /**
191
- * Represents a player instance in the networked environment.
192
- * Handles ownership, synchronization, and lifecycle management of player objects.
193
- */
194
162
  export class PlayerState extends Behaviour {
195
163
 
196
164
  private static _all: PlayerState[] = [];
197
- /** All PlayerState instances for all players in the scene */
165
+ /** all instances for all players */
198
166
  static get all(): PlayerState[] {
199
167
  return PlayerState._all;
200
168
  };
201
169
 
202
170
  private static _local: PlayerState[] = [];
203
- /** All PlayerState instances that belong to the local player */
171
+ /** all instances for the local player */
204
172
  static get local(): PlayerState[] {
205
173
  return PlayerState._local;
206
174
  }
207
175
 
208
- /**
209
- * Gets the PlayerState component for a given object or component
210
- * @param obj Object3D or Component to find the PlayerState for
211
- * @returns The PlayerState component if found, undefined otherwise
212
- */
213
176
  static getFor(obj: Object3D | Component) {
214
177
  if (obj instanceof Object3D) {
215
178
  return GameObject.getComponentInParent(obj, PlayerState);
@@ -220,34 +183,22 @@
220
183
  return undefined;
221
184
  }
222
185
 
223
- /**
224
- * Checks if a given object or component belongs to the local player
225
- * @param obj Object3D or Component to check
226
- * @returns True if the object belongs to the local player, false otherwise
227
- */
186
+ //** use to check if a component or gameobject is part of a instance owned by the local player */
228
187
  static isLocalPlayer(obj: Object3D | Component): boolean {
229
188
  const state = PlayerState.getFor(obj);
230
189
  return state?.isLocalPlayer ?? false;
231
190
  }
232
191
 
192
+ // static Callback
233
193
  private static _callbacks: { [key: string]: PlayerStateEventCallback[] } = {};
234
194
  /**
235
- * Registers a callback for a specific PlayerState event
236
- * @param event The event to listen for
237
- * @param cb Callback function that will be invoked when the event occurs
238
- * @returns The provided callback function for chaining
195
+ * Add a callback for a PlayerStateEvent
239
196
  */
240
197
  static addEventListener(event: PlayerStateEvent, cb: PlayerStateEventCallback) {
241
198
  if (!this._callbacks[event]) this._callbacks[event] = [];
242
199
  this._callbacks[event].push(cb);
243
200
  return cb;
244
201
  }
245
-
246
- /**
247
- * Removes a previously registered event callback
248
- * @param event The event type to remove the callback from
249
- * @param cb The callback function to remove
250
- */
251
202
  static removeEventListener(event: PlayerStateEvent, cb: PlayerStateEventCallback) {
252
203
  if (!this._callbacks[event]) return;
253
204
  const index = this._callbacks[event].indexOf(cb);
@@ -261,39 +212,20 @@
261
212
  }
262
213
 
263
214
 
264
- /** Event triggered when the owner of this PlayerState changes */
265
215
  public onOwnerChangeEvent = new EventList();
266
-
267
- /** Event triggered the first time an owner is assigned to this PlayerState */
268
216
  public onFirstOwnerChangeEvent = new EventList();
269
-
270
- /** Indicates if this PlayerState has an owner assigned */
271
217
  public hasOwner = false;
272
218
 
273
- /**
274
- * The connection ID of the player who owns this PlayerState instance
275
- * @syncField Synchronized across the network
276
- */
277
219
  @syncField(PlayerState.prototype.onOwnerChange)
278
220
  owner?: string;
279
221
 
280
- /**
281
- * When enabled, PlayerState will not destroy itself when the owner is not connected anymore
282
- */
222
+ /** when enabled PlayerSync will not destroy itself when not connected anymore */
283
223
  dontDestroy: boolean = false;
284
224
 
285
- /**
286
- * Indicates if this PlayerState belongs to the local player
287
- */
288
225
  get isLocalPlayer(): boolean {
289
226
  return this.owner === this.context.connection.connectionId;
290
227
  }
291
228
 
292
- /**
293
- * Handles owner change events and updates relevant state
294
- * @param newOwner The new owner's connection ID
295
- * @param oldOwner The previous owner's connection ID
296
- */
297
229
  private onOwnerChange(newOwner: string, oldOwner: string) {
298
230
  if (debug) console.log(`PlayerSync.onOwnerChange: ${oldOwner} → ${newOwner} (me: ${this.context.connection.connectionId})`);
299
231
 
@@ -368,7 +300,7 @@
368
300
  }
369
301
  }
370
302
 
371
- /** Tells the server that this client has been destroyed, and the networking message for the instantiate will be removed */
303
+ /** this tells the server that this client has been destroyed and the networking message for the instantiate will be removed */
372
304
  doDestroy() {
373
305
  if (debug) console.log("PlayerSync.doDestroy → syncDestroy", this.name);
374
306
  syncDestroy(this.gameObject, this.context.connection, true, { saveInRoom: false });
@@ -387,10 +319,6 @@
387
319
  }
388
320
  }
389
321
 
390
- /**
391
- * Handler for when a user leaves the networked room
392
- * @param model Object containing the ID of the user who left
393
- */
394
322
  private onUserLeftRoom = (model: { userId: string }) => {
395
323
  if (model.userId === this.owner) {
396
324
  if (debug)
src/engine-components/postprocessing/PostProcessingEffect.ts CHANGED
@@ -75,10 +75,6 @@
75
75
 
76
76
  abstract get typeName(): string;
77
77
 
78
- /**
79
- * Whether the effect is active or not. Prefer using `enabled` instead.
80
- * @deprecated
81
- */
82
78
  @serializable()
83
79
  active: boolean = true;
84
80
 
@@ -86,16 +82,14 @@
86
82
 
87
83
  onEnable(): void {
88
84
  super.onEnable();
89
- if (debug) console.warn("onEnable effect", this, this.__internalDidAwakeAndStart)
85
+ this.onEffectEnabled();
90
86
  // Dont override the serialized value by enabling (we could also just disable this component / map enabled to active)
91
87
  if (this.__internalDidAwakeAndStart)
92
88
  this.active = true;
93
- this.onEffectEnabled();
94
89
  }
95
90
 
96
91
  onDisable(): void {
97
92
  super.onDisable();
98
- if (debug) console.warn("onDisable effect", this)
99
93
  this._manager?.removeEffect(this);
100
94
  this.active = false;
101
95
  }
src/engine-components/postprocessing/PostProcessingHandler.ts CHANGED
@@ -219,12 +219,12 @@
219
219
  if (ef instanceof MODULES.POSTPROCESSING.MODULE.Effect)
220
220
  effects.push(ef as Effect);
221
221
  else if (ef instanceof MODULES.POSTPROCESSING.MODULE.Pass) {
222
- // const pass = new MODULES.POSTPROCESSING.MODULE.EffectPass(cam, ...effects);
223
- // pass.mainScene = scene;
224
- // pass.name = effects.map(e => e.constructor.name).join(", ");
225
- // pass.enabled = true;
222
+ const pass = new MODULES.POSTPROCESSING.MODULE.EffectPass(cam, ...effects);
223
+ pass.mainScene = scene;
224
+ pass.name = effects.map(e => e.constructor.name).join(", ");
225
+ pass.enabled = true;
226
226
  // composer.addPass(pass);
227
- // effects.length = 0;
227
+ effects.length = 0;
228
228
  composer.addPass(ef as Pass);
229
229
  }
230
230
  else {
@@ -262,7 +262,7 @@
262
262
  }
263
263
 
264
264
  if (debug)
265
- console.log("[PostProcessing] Passes ", composer.passes);
265
+ console.log("PostProcessing Passes", effectsOrPasses, "->", composer.passes);
266
266
  }
267
267
 
268
268
  private orderEffects() {
src/engine-components/RigidBody.ts CHANGED
@@ -498,9 +498,7 @@
498
498
  else this.setAngularVelocity(x);
499
499
  }
500
500
 
501
- /**
502
- * Returns the rigidbody velocity smoothed over ~ 10 frames
503
- */
501
+ /** Returns the rigidbody velocity smoothed over ~ 10 frames */
504
502
  public get smoothedVelocity(): Vector3 {
505
503
  this._smoothedVelocityGetter.copy(this._smoothedVelocity);
506
504
  return this._smoothedVelocityGetter.multiplyScalar(1 / this.context.time.deltaTime);
@@ -514,9 +512,9 @@
514
512
 
515
513
 
516
514
  private captureVelocity() {
517
- const matrixWorld = this.gameObject.matrixWorld;
518
- Rigidbody.tempPosition.setFromMatrixPosition(matrixWorld);
519
- const vel = Rigidbody.tempPosition.sub(this._lastPosition);
515
+ const wp = getWorldPosition(this.gameObject);
516
+ this.gameObject.matrixWorld.decompose(Rigidbody.tempPosition, tempQuaternion, tempScale);
517
+ const vel = wp.sub(this._lastPosition);
520
518
  this._lastPosition.copy(Rigidbody.tempPosition);
521
519
  this._smoothedVelocity.lerp(vel, this.context.time.deltaTime / .1);
522
520
  }
src/engine-components/SceneSwitcher.ts CHANGED
@@ -46,10 +46,6 @@
46
46
  index: number;
47
47
  }
48
48
 
49
- export type LoadSceneProgressEvent = LoadSceneEvent & {
50
- progress: number,
51
- }
52
-
53
49
  /**
54
50
  * The ISceneEventListener is called by the {@link SceneSwitcher} when a scene is loaded or unloaded.
55
51
  * It must be added to the root object of your scene (that is being loaded) or on the same object as the SceneSwitcher
@@ -230,15 +226,6 @@
230
226
  get currentlyLoadedScene() { return this._currentScene; }
231
227
 
232
228
  /**
233
- * Called when a scene starts loading
234
- */
235
- @serializable(EventList)
236
- sceneLoadingStart: EventList<LoadSceneEvent> = new EventList();
237
-
238
- @serializable(EventList)
239
- sceneLoadingProgress: EventList<ProgressEvent> = new EventList();
240
-
241
- /**
242
229
  * The sceneLoaded event is called when a scene/glTF is loaded and added to the scene
243
230
  */
244
231
  @serializable(EventList)
@@ -294,7 +281,7 @@
294
281
  this._preloadScheduler.maxLoadAhead = this.preloadNext;
295
282
  this._preloadScheduler.maxLoadBehind = this.preloadPrevious;
296
283
  this._preloadScheduler.maxConcurrent = this.preloadConcurrent;
297
- this._preloadScheduler.begin(2000);
284
+ this._preloadScheduler.begin();
298
285
 
299
286
  // Begin loading the loading scene
300
287
  if (this.autoLoadFirstScene && this._currentIndex === -1 && !await this.tryLoadFromQueryParam()) {
@@ -615,13 +602,11 @@
615
602
 
616
603
  const loadStartEvt = new CustomEvent<LoadSceneEvent>("loadscene-start", { detail: { scene: scene, switcher: this, index: index } })
617
604
  this.dispatchEvent(loadStartEvt);
618
- this.sceneLoadingStart?.invoke(loadStartEvt.detail);
619
605
  await this.onStartLoading();
620
606
  // start loading and wait for the scene to be loaded
621
607
  await scene.loadAssetAsync((_, prog) => {
622
608
  this._currentLoadingProgress = prog;
623
609
  this.dispatchEvent(prog);
624
- this.sceneLoadingProgress?.invoke(prog);
625
610
  }).catch(console.error);
626
611
  await this.onEndLoading();
627
612
  const finishedEvt = new CustomEvent<LoadSceneEvent>("loadscene-finished", { detail: { scene: scene, switcher: this, index: index } });
@@ -857,18 +842,9 @@
857
842
 
858
843
 
859
844
 
860
- /**
861
- * PreLoadScheduler is responsible for managing preloading of scenes.
862
- * It handles scheduling and limiting concurrent downloads of scenes based on specified parameters.
863
- */
864
845
  class PreLoadScheduler {
865
- /** Maximum number of scenes to preload ahead of the current scene */
866
846
  maxLoadAhead: number;
867
-
868
- /** Maximum number of scenes to preload behind the current scene */
869
847
  maxLoadBehind: number;
870
-
871
- /** Maximum number of scenes that can be preloaded concurrently */
872
848
  maxConcurrent: number;
873
849
 
874
850
  private _isRunning: boolean = false;
@@ -876,13 +852,6 @@
876
852
  private _loadTasks: LoadTask[] = [];
877
853
  private _maxConcurrentLoads: number = 1;
878
854
 
879
- /**
880
- * Creates a new PreLoadScheduler instance
881
- * @param rooms The SceneSwitcher that this scheduler belongs to
882
- * @param ahead Number of scenes to preload ahead of current scene
883
- * @param behind Number of scenes to preload behind current scene
884
- * @param maxConcurrent Maximum number of concurrent preloads allowed
885
- */
886
855
  constructor(rooms: SceneSwitcher, ahead: number = 1, behind: number = 1, maxConcurrent: number = 2) {
887
856
  this._switcher = rooms;
888
857
  this.maxLoadAhead = ahead;
@@ -890,34 +859,26 @@
890
859
  this.maxConcurrent = maxConcurrent;
891
860
  }
892
861
 
893
- /**
894
- * Starts the preloading process after a specified delay
895
- * @param delay Time in milliseconds to wait before starting preload
896
- */
897
- begin(delay: number) {
862
+ begin() {
898
863
  if (this._isRunning) return;
899
- if (debug) console.log("Preload begin", { delay })
864
+ if (debug) console.log("Preload begin")
900
865
  this._isRunning = true;
901
- let lastRoom: number = -10;
866
+ let lastRoom: number = -1;
902
867
  let searchDistance: number;
903
868
  let searchCall: number;
904
869
  const array = this._switcher.scenes;
905
- const startTime = Date.now() + delay;
906
870
  const interval = setInterval(() => {
907
871
  if (this.allLoaded()) {
908
872
  if (debug)
909
- console.log("All scenes (pre-)loaded");
873
+ console.log("All scenes loaded");
910
874
  this.stop();
911
875
  }
912
876
  if (!this._isRunning) {
913
877
  clearInterval(interval);
914
878
  return;
915
879
  }
916
-
917
- if (Date.now() < startTime) return;
918
-
919
880
  if (this.canLoadNewScene() === false) return;
920
- if (lastRoom === -10 || lastRoom !== this._switcher.currentIndex) {
881
+ if (lastRoom !== this._switcher.currentIndex) {
921
882
  lastRoom = this._switcher.currentIndex;
922
883
  searchCall = 0;
923
884
  searchDistance = 0;
@@ -931,33 +892,19 @@
931
892
  if (roomIndex < 0) return;
932
893
  // if (roomIndex < 0) roomIndex = array.length + roomIndex;
933
894
  if (roomIndex < 0 || roomIndex >= array.length) return;
934
- if (!this._loadTasks.some(t => t.index === roomIndex)) {
935
- const scene = array[roomIndex];
936
- if (debug) console.log("Preload scene", { roomIndex, searchForward, lastRoom, currentIndex: this._switcher.currentIndex, tasks: this._loadTasks.length }, scene?.url);
937
- new LoadTask(roomIndex, scene, this._loadTasks);
938
- }
895
+ const scene = array[roomIndex];
896
+ new LoadTask(roomIndex, scene, this._loadTasks);
939
897
  }, 200);
940
898
  }
941
899
 
942
- /**
943
- * Stops the preloading process
944
- */
945
900
  stop() {
946
901
  this._isRunning = false;
947
902
  }
948
903
 
949
- /**
950
- * Checks if a new scene can be loaded based on current load limits
951
- * @returns True if a new scene can be loaded, false otherwise
952
- */
953
904
  canLoadNewScene(): boolean {
954
905
  return this._loadTasks.length < this._maxConcurrentLoads;
955
906
  }
956
907
 
957
- /**
958
- * Checks if all scenes in the SceneSwitcher have been loaded
959
- * @returns True if all scenes are loaded, false otherwise
960
- */
961
908
  allLoaded(): boolean {
962
909
  if (this._switcher.scenes) {
963
910
  for (const scene of this._switcher.scenes) {
@@ -968,25 +915,12 @@
968
915
  }
969
916
  }
970
917
 
971
- /**
972
- * Represents a single preloading task for a scene
973
- */
974
918
  class LoadTask {
975
- /** The index of the scene in the scenes array */
919
+
976
920
  index: number;
977
-
978
- /** The AssetReference to be loaded */
979
921
  asset: AssetReference;
980
-
981
- /** The collection of active load tasks this task belongs to */
982
922
  tasks: LoadTask[];
983
923
 
984
- /**
985
- * Creates a new LoadTask and begins loading immediately
986
- * @param index The index of the scene in the scenes array
987
- * @param asset The AssetReference to preload
988
- * @param tasks The collection of active load tasks
989
- */
990
924
  constructor(index: number, asset: AssetReference, tasks: LoadTask[]) {
991
925
  this.index = index;
992
926
  this.asset = asset;
@@ -995,9 +929,6 @@
995
929
  this.awaitLoading();
996
930
  }
997
931
 
998
- /**
999
- * Asynchronously loads the asset and removes this task from the active tasks list when complete
1000
- */
1001
932
  private async awaitLoading() {
1002
933
  if (this.asset && !this.asset.isLoaded()) {
1003
934
  if (debug)
src/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusionN8.ts CHANGED
@@ -1,16 +1,13 @@
1
1
  import type { N8AOPostPass } from "n8ao";
2
- import { Color, DepthFormat, DepthStencilFormat, DepthTexture, PerspectiveCamera, UnsignedInt248Type, UnsignedIntType, WebGLRenderTarget } from "three";
2
+ import { Color, PerspectiveCamera } from "three";
3
3
 
4
4
  import { MODULES } from "../../../engine/engine_modules.js";
5
5
  import { serializable } from "../../../engine/engine_serialization.js";
6
6
  import { validate } from "../../../engine/engine_util_decorator.js";
7
- import { getParam } from "../../../engine/engine_utils.js";
8
7
  import { type EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect.js";
9
8
  import { VolumeParameter } from "../VolumeParameter.js";
10
9
  import { registerCustomEffectType } from "../VolumeProfile.js";
11
10
 
12
- const debugN8AO = getParam("debugN8AO");
13
-
14
11
  // https://github.com/N8python/n8ao
15
12
 
16
13
  /**See (N8AO documentation)[https://github.com/N8python/n8ao] */
@@ -33,10 +30,6 @@
33
30
  return "ScreenSpaceAmbientOcclusionN8";
34
31
  }
35
32
 
36
- get pass(): N8AOPostPass {
37
- return this._ssao;
38
- }
39
-
40
33
  @validate()
41
34
  @serializable()
42
35
  gammaCorrection: boolean = true;
@@ -116,51 +109,19 @@
116
109
 
117
110
  const cam = this.context.mainCamera! as PerspectiveCamera;
118
111
 
119
- const width = this.context.domWidth;
120
- const height = this.context.domHeight;
121
-
122
112
  const ssao = this._ssao = new MODULES.POSTPROCESSING_AO.MODULE.N8AOPostPass(
123
113
  this.context.scene,
124
114
  cam,
125
- width, height
126
- ) as N8AOPostPass;
127
-
115
+ this.context.domWidth,
116
+ this.context.domHeight
117
+ );
128
118
  const mode = ScreenSpaceAmbientOcclusionN8QualityMode[this.quality];
129
119
  ssao.setQualityMode(mode);
130
120
 
131
- ssao.configuration.transparencyAware = false;
132
- // ssao.needsSwap = false;
133
-
121
+ ssao.configuration.gammaCorrection = this.gammaCorrection;
134
122
 
135
- const renderTarget = new WebGLRenderTarget(width, height);
136
- ssao.configuration.beautyRenderTarget = renderTarget;
137
- ssao.configuration.autoRenderBeauty = false;
138
- // // If you just want a depth buffer
139
- // renderTarget.depthTexture = new DepthTexture(width, height, UnsignedIntType);
140
- // renderTarget.depthTexture.format = DepthFormat;
141
- // // If you want a depth buffer and a stencil buffer
142
- // renderTarget.depthTexture = new DepthTexture(width, height, UnsignedInt248Type);
143
- // renderTarget.depthTexture.format = DepthStencilFormat;
144
-
145
-
146
- ssao.configuration.gammaCorrection = this.gammaCorrection;
147
123
  ssao.configuration.screenSpaceRadius = this.screenspaceRadius;
148
124
 
149
-
150
- if (debugN8AO) {
151
- // ssao.debug = debugN8AO;
152
- ssao.enableDebugMode();
153
- console.log(ssao);
154
- setInterval(() => {
155
- console.log("SSAO", ssao.lastTime);
156
- }, 1000);
157
- setInterval(() => {
158
- // ssao.enabled = !ssao.enabled;
159
- console.log("SSAO", ssao.enabled, { ssao, autoRenderBeauty: ssao.configuration.autoRenderBeauty });
160
- }, 4000)
161
- }
162
-
163
-
164
125
  this.intensity.onValueChanged = newValue => {
165
126
  ssao.configuration.intensity = newValue;
166
127
  }
@@ -174,18 +135,10 @@
174
135
  if (!ssao.color) ssao.color = new Color();
175
136
  ssao.configuration.color.copy(newValue);
176
137
  }
177
-
178
138
 
179
- // const normalPass = new MODULES.POSTPROCESSING.MODULE.NormalPass(this.context.scene, cam);
180
- // const depthDownsamplingPass = new MODULES.POSTPROCESSING.MODULE.DepthDownsamplingPass({
181
- // normalBuffer: normalPass.texture,
182
- // resolutionScale: .1
183
- // });
184
- // const arr = new Array();
185
- // arr.push(normalPass);
186
- // arr.push(depthDownsamplingPass);
187
- // arr.push(ssao);
188
- return ssao;
139
+ const arr = new Array();
140
+ arr.push(ssao);
141
+ return arr;
189
142
  }
190
143
 
191
144
  }
src/engine-components/SpatialTrigger.ts CHANGED
@@ -8,15 +8,8 @@
8
8
 
9
9
  const debug = getParam("debugspatialtrigger");
10
10
 
11
- /** Layer instances used for mask comparison */
12
11
  const layer1 = new Layers();
13
12
  const layer2 = new Layers();
14
- /**
15
- * Tests if two layer masks intersect
16
- * @param mask1 First layer mask
17
- * @param mask2 Second layer mask
18
- * @returns True if the layers intersect
19
- */
20
13
  function testMask(mask1, mask2) {
21
14
  layer1.mask = mask1;
22
15
  layer2.mask = mask2;
@@ -24,45 +17,25 @@
24
17
  }
25
18
 
26
19
  /**
27
- * Component that receives and responds to spatial events, like entering or exiting a trigger zone.
28
- * Used in conjunction with {@link SpatialTrigger} to create interactive spatial events.
29
20
  * @category Interactivity
30
21
  * @group Components
31
22
  */
32
23
  export class SpatialTriggerReceiver extends Behaviour {
33
24
 
34
- /**
35
- * Bitmask determining which triggers this receiver responds to
36
- * Only triggers with matching masks will interact with this receiver
37
- */
25
+ // currently Layers in unity but maybe this should be a string or plane number? Or should it be a bitmask to allow receivers use multiple triggers?
38
26
  @serializable()
39
27
  triggerMask: number = 0;
40
-
41
- /** Event invoked when this object enters a trigger zone */
42
28
  @serializable(EventList)
43
29
  onEnter?: EventList<any>;
44
-
45
- /** Event invoked continuously while this object is inside a trigger zone */
46
30
  @serializable(EventList)
47
31
  onStay?: EventList<any>;
48
-
49
- /** Event invoked when this object exits a trigger zone */
50
32
  @serializable(EventList)
51
33
  onExit?: EventList<any>;
52
34
 
53
- /**
54
- * Initializes the receiver and logs debug info if enabled
55
- * @internal
56
- */
57
35
  start() {
58
36
  if (debug) console.log(this.name, this.triggerMask, this);
59
37
  }
60
38
 
61
- /**
62
- * Checks for intersections with spatial triggers and fires appropriate events
63
- * Handles enter, stay, and exit events for all relevant triggers
64
- * @internal
65
- */
66
39
  update(): void {
67
40
  this.currentIntersected.length = 0;
68
41
  for (const trigger of SpatialTrigger.triggers) {
@@ -86,38 +59,23 @@
86
59
  }
87
60
  this.lastIntersected.length = 0;
88
61
  this.lastIntersected.push(...this.currentIntersected);
62
+
89
63
  }
90
64
 
91
- /** Array of triggers currently intersecting with this receiver */
92
65
  readonly currentIntersected: SpatialTrigger[] = [];
93
-
94
- /** Array of triggers that intersected with this receiver in the previous frame */
95
66
  readonly lastIntersected: SpatialTrigger[] = [];
96
67
 
97
- /**
98
- * Handles trigger enter events.
99
- * @param trigger The spatial trigger that was entered
100
- */
101
68
  onEnterTrigger(trigger: SpatialTrigger): void {
102
69
  if(debug) console.log("ENTER TRIGGER", this.name, trigger.name, this, trigger);
103
70
  trigger.raiseOnEnterEvent(this);
104
71
  this.onEnter?.invoke();
105
72
  }
106
-
107
- /**
108
- * Handles trigger exit events.
109
- * @param trigger The spatial trigger that was exited
110
- */
111
73
  onExitTrigger(trigger: SpatialTrigger): void {
112
74
  if(debug) console.log("EXIT TRIGGER", this.name, trigger.name, );
113
75
  trigger.raiseOnExitEvent(this);
114
76
  this.onExit?.invoke();
115
77
  }
116
78
 
117
- /**
118
- * Handles trigger stay events.
119
- * @param trigger The spatial trigger that the receiver is staying in
120
- */
121
79
  onStayTrigger(trigger: SpatialTrigger): void {
122
80
  trigger.raiseOnStayEvent(this);
123
81
  this.onStay?.invoke();
@@ -125,38 +83,24 @@
125
83
  }
126
84
 
127
85
  /**
128
- * A spatial trigger component that detects objects within a box-shaped area.
129
- * Used to trigger events when objects enter, stay in, or exit the defined area
86
+ * A trigger that can be used to detect if an object is inside a box.
130
87
  * @category Interactivity
131
88
  * @group Components
132
89
  */
133
90
  export class SpatialTrigger extends Behaviour {
134
91
 
135
- /** Global registry of all active spatial triggers in the scene */
136
92
  static triggers: SpatialTrigger[] = [];
137
93
 
138
- /**
139
- * Bitmask determining which receivers this trigger affects.
140
- * Only receivers with matching masks will be triggered.
141
- */
142
- // currently Layers in unity but maybe this should be a string or plane number? Or should it be a bitmask to allow receivers use multiple triggers?
143
94
  @serializable()
144
95
  triggerMask?: number;
145
96
 
146
- /** Box helper component used to visualize and calculate the trigger area */
147
97
  private boxHelper?: BoxHelperComponent;
148
98
 
149
- /**
150
- * Initializes the trigger and logs debug info if enabled
151
- */
152
99
  start() {
153
100
  if (debug)
154
101
  console.log(this.name, this.triggerMask, this);
155
102
  }
156
103
 
157
- /**
158
- * Registers this trigger in the global registry and sets up debug visualization if enabled
159
- */
160
104
  onEnable(): void {
161
105
  SpatialTrigger.triggers.push(this);
162
106
  if (!this.boxHelper) {
@@ -164,19 +108,10 @@
164
108
  this.boxHelper?.showHelper(null, debug as boolean);
165
109
  }
166
110
  }
167
-
168
- /**
169
- * Removes this trigger from the global registry when disabled
170
- */
171
111
  onDisable(): void {
172
112
  SpatialTrigger.triggers.splice(SpatialTrigger.triggers.indexOf(this), 1);
173
113
  }
174
114
 
175
- /**
176
- * Tests if an object is inside this trigger's box
177
- * @param obj The object to test against this trigger
178
- * @returns True if the object is inside the trigger box
179
- */
180
115
  test(obj: Object3D): boolean {
181
116
  if (!this.boxHelper) return false;
182
117
  return this.boxHelper.isInBox(obj) ?? false;
@@ -184,10 +119,6 @@
184
119
 
185
120
  // private args: SpatialTriggerEventArgs = new SpatialTriggerEventArgs();
186
121
 
187
- /**
188
- * Raises the onEnter event on any SpatialTriggerReceiver components attached to this trigger's GameObject
189
- * @param rec The receiver that entered this trigger
190
- */
191
122
  raiseOnEnterEvent(rec: SpatialTriggerReceiver) {
192
123
  // this.args.trigger = this;
193
124
  // this.args.source = rec;
@@ -199,10 +130,6 @@
199
130
  }, false);
200
131
  }
201
132
 
202
- /**
203
- * Raises the onStay event on any SpatialTriggerReceiver components attached to this trigger's GameObject
204
- * @param rec The receiver that is staying in this trigger
205
- */
206
133
  raiseOnStayEvent(rec: SpatialTriggerReceiver) {
207
134
  // this.args.trigger = this;
208
135
  // this.args.source = rec;
@@ -214,10 +141,6 @@
214
141
  }, false);
215
142
  }
216
143
 
217
- /**
218
- * Raises the onExit event on any SpatialTriggerReceiver components attached to this trigger's GameObject
219
- * @param rec The receiver that exited this trigger
220
- */
221
144
  raiseOnExitEvent(rec: SpatialTriggerReceiver) {
222
145
  GameObject.foreachComponent(this.gameObject, c => {
223
146
  if (c === rec) return;
src/engine-components/SpectatorCamera.ts CHANGED
@@ -17,77 +17,50 @@
17
17
  import { XRStateFlag } from "./webxr/XRFlag.js";
18
18
 
19
19
 
20
- /**
21
- * Defines the viewing perspective in spectator mode
22
- */
23
20
  export enum SpectatorMode {
24
- /** View from the perspective of the followed player */
25
21
  FirstPerson = 0,
26
- /** Freely view from a third-person perspective */
27
22
  ThirdPerson = 1,
28
23
  }
29
24
 
30
25
  const debug = getParam("debugspectator");
31
26
 
32
27
  /**
33
- * Provides functionality to follow and spectate other users in a networked environment.
34
- * Handles camera switching, following behavior, and network synchronization for spectator mode.
35
- *
36
- * Debug mode can be enabled with the URL parameter `?debugspectator`, which provides additional console output.
37
- *
38
28
  * @category Networking
39
29
  * @group Components
40
30
  */
41
31
  export class SpectatorCamera extends Behaviour {
42
32
 
43
- /** Reference to the Camera component on this GameObject */
44
33
  cam: Camera | null = null;
45
34
 
46
- /**
47
- * When enabled, pressing F will send a request to all connected users to follow the local player.
48
- * Pressing ESC will stop spectating.
49
- */
35
+ /** when enabled pressing F will send a request to all connected users to follow me, ESC to stop */
50
36
  @serializable()
51
37
  useKeys: boolean = true;
52
38
 
53
39
  private _mode: SpectatorMode = SpectatorMode.FirstPerson;
54
40
 
55
- /** Gets the current spectator perspective mode */
56
41
  get mode() { return this._mode; }
57
- /** Sets the current spectator perspective mode */
58
42
  set mode(val: SpectatorMode) {
59
43
  this._mode = val;
60
44
  }
61
45
 
62
- /** Returns whether this user is currently spectating another user */
46
+ /** if this user is currently spectating someone else */
63
47
  get isSpectating(): boolean {
64
48
  return this._handler?.currentTarget !== undefined;
65
49
  }
66
50
 
67
- /**
68
- * Checks if this instance is spectating the user with the given ID
69
- * @param userId The user ID to check
70
- * @returns True if spectating the specified user, false otherwise
71
- */
72
51
  isSpectatingUser(userId: string): boolean {
73
52
  return this.target?.userId === userId;
74
53
  }
75
54
 
76
- /**
77
- * Checks if the user with the specified ID is following this user
78
- * @param userId The user ID to check
79
- * @returns True if the specified user is following this user, false otherwise
80
- */
81
55
  isFollowedBy(userId: string): boolean {
82
56
  return this.followers?.includes(userId);
83
57
  }
84
58
 
85
- /** List of user IDs that are currently following the user */
59
+ /** list of other users that are following me */
86
60
  get followers(): string[] {
87
61
  return this._networking.followers;
88
62
  }
89
63
 
90
- /** Stops the current spectating session */
91
64
  stopSpectating() {
92
65
  if (this.context.isInXR) {
93
66
  this.followSelf();
@@ -96,15 +69,11 @@
96
69
  this.target = undefined;
97
70
  }
98
71
 
99
- /** Gets the local player's connection ID */
100
72
  private get localId() : string {
101
73
  return this.context.connection.connectionId ?? "local";
102
74
  }
103
75
 
104
- /**
105
- * Sets the player view to follow
106
- * @param target The PlayerView to follow, or undefined to stop spectating
107
- */
76
+ /** player view to follow */
108
77
  set target(target: PlayerView | undefined) {
109
78
  if (this._handler) {
110
79
 
@@ -137,17 +106,14 @@
137
106
  }
138
107
  }
139
108
 
140
- /** Gets the currently followed player view */
141
109
  get target(): PlayerView | undefined {
142
110
  return this._handler?.currentTarget;
143
111
  }
144
112
 
145
- /** Sends a network request for all users to follow this player */
146
113
  requestAllFollowMe() {
147
114
  this._networking.onRequestFollowMe();
148
115
  }
149
116
 
150
- /** Determines if the camera is spectating the local player */
151
117
  private get isSpectatingSelf() {
152
118
  return this.isSpectating && this.target?.currentObject === this.context.players.getPlayerView(this.localId)?.currentObject;
153
119
  }
@@ -165,6 +131,7 @@
165
131
  private _networking!: SpectatorCamNetworking;
166
132
 
167
133
  awake(): void {
134
+
168
135
  this._debug = new SpectatorSelectionController(this.context, this);
169
136
  this._networking = new SpectatorCamNetworking(this.context, this);
170
137
  this._networking.awake();
@@ -177,6 +144,7 @@
177
144
  return;
178
145
  }
179
146
 
147
+
180
148
  if (!this._handler && this.cam)
181
149
  this._handler = new SpectatorHandler(this.context, this.cam, this);
182
150
 
@@ -189,10 +157,6 @@
189
157
  this._networking?.destroy();
190
158
  }
191
159
 
192
- /**
193
- * Checks if the current platform supports spectator mode
194
- * @returns True if the platform is supported, false otherwise
195
- */
196
160
  private isSupportedPlatform() {
197
161
  const ua = window.navigator.userAgent;
198
162
  const standalone = /Windows|MacOS/.test(ua);
@@ -200,19 +164,12 @@
200
164
  return standalone && !isHololens;
201
165
  }
202
166
 
203
- /**
204
- * Called before entering WebXR mode
205
- * @param _evt The WebXR event
206
- */
207
167
  onBeforeXR(_evt) {
208
168
  if (!this.isSupportedPlatform()) return;
209
169
  GameObject.setActive(this.gameObject, true);
210
170
  }
211
171
 
212
- /**
213
- * Called when entering WebXR mode
214
- * @param _evt The WebXR event
215
- */
172
+
216
173
  onEnterXR(_evt) {
217
174
  if (!this.isSupportedPlatform()) return;
218
175
  if (debug) console.log(this.context.mainCamera);
@@ -221,10 +178,6 @@
221
178
  }
222
179
  }
223
180
 
224
- /**
225
- * Called when exiting WebXR mode
226
- * @param _evt The WebXR event
227
- */
228
181
  onLeaveXR(_evt) {
229
182
  this.context.removeCamera(this.cam as ICamera);
230
183
  GameObject.setActive(this.gameObject, false);
@@ -235,9 +188,7 @@
235
188
  this.stopSpectating();
236
189
  }
237
190
 
238
- /**
239
- * Sets the target to follow the local player
240
- */
191
+
241
192
  private followSelf() {
242
193
  this.target = this.context.players.getPlayerView(this.context.connection.connectionId);
243
194
  if (!this.target) {
@@ -250,9 +201,6 @@
250
201
  // TODO: only show Spectator cam for DesktopVR;
251
202
  // don't show for AR, don't show on Quest
252
203
  // TODO: properly align cameras on enter/exit VR, seems currently spectator cam breaks alignment
253
- /**
254
- * Called after the main rendering pass to render the spectator view
255
- */
256
204
  onAfterRender(): void {
257
205
  if (!this.cam) return;
258
206
 
@@ -330,9 +278,6 @@
330
278
  this.resetAvatarFlags();
331
279
  }
332
280
 
333
- /**
334
- * Updates avatar visibility flags for rendering in spectator mode
335
- */
336
281
  private setAvatarFlagsBeforeRender() {
337
282
  const isFirstPersonMode = this._mode === SpectatorMode.FirstPerson;
338
283
 
@@ -350,9 +295,6 @@
350
295
  }
351
296
  }
352
297
 
353
- /**
354
- * Restores avatar visibility flags after spectator rendering
355
- */
356
298
  private resetAvatarFlags() {
357
299
  for (const av of AvatarMarker.instances) {
358
300
  if (av.avatar && "flags" in av.avatar) {
@@ -371,9 +313,6 @@
371
313
  }
372
314
  }
373
315
 
374
- /**
375
- * Interface for handling spectator camera behavior
376
- */
377
316
  interface ISpectatorHandler {
378
317
  context: Context;
379
318
  get currentTarget(): PlayerView | undefined;
@@ -383,9 +322,6 @@
383
322
  destroy();
384
323
  }
385
324
 
386
- /**
387
- * Handles the smooth following behavior for the spectator camera
388
- */
389
325
  class SpectatorHandler implements ISpectatorHandler {
390
326
 
391
327
  readonly context: Context;
@@ -397,7 +333,6 @@
397
333
  private view?: PlayerView;
398
334
  private currentObject: Object3D | undefined;
399
335
 
400
- /** Gets the currently targeted player view */
401
336
  get currentTarget(): PlayerView | undefined {
402
337
  return this.view;
403
338
  }
@@ -408,10 +343,6 @@
408
343
  this.spectator = spectator;
409
344
  }
410
345
 
411
- /**
412
- * Sets the target player view to follow
413
- * @param view The PlayerView to follow
414
- */
415
346
  set(view?: PlayerView): void {
416
347
  const followObject = view?.currentObject;
417
348
  if (!followObject) {
@@ -437,7 +368,6 @@
437
368
  else this.context.removeCamera(this.cam as ICamera);
438
369
  }
439
370
 
440
- /** Disables the spectator following behavior */
441
371
  disable() {
442
372
  if (debug) console.log("STOP FOLLOW", this.currentObject);
443
373
  this.view = undefined;
@@ -447,17 +377,12 @@
447
377
  this.follow.enabled = false;
448
378
  }
449
379
 
450
- /** Cleans up resources used by the handler */
451
380
  destroy() {
452
381
  this.target?.removeFromParent();
453
382
  if (this.follow)
454
383
  GameObject.destroy(this.follow);
455
384
  }
456
385
 
457
- /**
458
- * Updates the camera position and orientation based on the spectator mode
459
- * @param mode The current spectator mode (first or third person)
460
- */
461
386
  update(mode: SpectatorMode) {
462
387
  if (this.currentTarget?.isConnected === false || this.currentTarget?.removed === true) {
463
388
  if (debug) console.log("Target disconnected or timeout", this.currentTarget);
@@ -506,13 +431,13 @@
506
431
  target.quaternion.copy(_inverseYQuat);
507
432
  else target.quaternion.identity();
508
433
  }
434
+
435
+
509
436
  }
510
437
 
511
438
  const _inverseYQuat = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
512
439
 
513
- /**
514
- * Handles user input for selecting targets to spectate
515
- */
440
+
516
441
  class SpectatorSelectionController {
517
442
 
518
443
  private readonly context: Context;
@@ -543,9 +468,6 @@
543
468
  });
544
469
  }
545
470
 
546
- /**
547
- * Attempts to select an avatar to spectate through raycasting
548
- */
549
471
  private trySelectObject() {
550
472
  const opts = new RaycastOptions();
551
473
  opts.setMask(0xffffff);
@@ -569,17 +491,17 @@
569
491
  }
570
492
  }
571
493
 
572
- /**
573
- * Network model for communicating follower changes
574
- */
494
+
495
+
496
+
497
+
575
498
  class SpectatorFollowerChangedEventModel implements IModel {
576
- /** The user ID that is following */
499
+ /** the user that is following */
577
500
  guid: string;
578
501
  readonly dontSave: boolean = true;
579
502
 
580
- /** The user ID being followed */
503
+ /** the user being followed */
581
504
  targetUserId: string | undefined;
582
- /** Indicates if the user stopped following */
583
505
  stoppedFollowing: boolean;
584
506
 
585
507
  constructor(connectionId: string, userId: string | undefined, stoppedFollowing: boolean) {
@@ -589,9 +511,6 @@
589
511
  }
590
512
  }
591
513
 
592
- /**
593
- * Network model for requesting users to follow a specific player
594
- */
595
514
  class SpectatorFollowEventModel implements IModel {
596
515
  guid: string;
597
516
  userId: string | undefined;
@@ -602,14 +521,11 @@
602
521
  }
603
522
  }
604
523
 
605
- /**
606
- * Handles network communication for spectator functionality
607
- */
608
524
  class SpectatorCamNetworking {
609
525
 
610
- /** List of user IDs currently following this player */
611
526
  readonly followers: string[] = [];
612
527
 
528
+
613
529
  private readonly context: Context;
614
530
  private readonly spectator: SpectatorCamera;
615
531
  private _followerEventMethod: Function;
@@ -624,9 +540,6 @@
624
540
  this._joinedRoomMethod = this.onUserJoinedRoom.bind(this);
625
541
  }
626
542
 
627
- /**
628
- * Initializes network event listeners
629
- */
630
543
  awake() {
631
544
  this.context.connection.beginListen("spectator-follower-changed", this._followerEventMethod);
632
545
  this.context.connection.beginListen("spectator-request-follow", this._requestFollowMethod);
@@ -642,20 +555,12 @@
642
555
  });
643
556
  }
644
557
 
645
- /**
646
- * Removes network event listeners
647
- */
648
558
  destroy() {
649
559
  this.context.connection.stopListen("spectator-follower-changed", this._followerEventMethod);
650
560
  this.context.connection.stopListen("spectator-request-follow", this._requestFollowMethod);
651
561
  this.context.connection.stopListen(RoomEvents.JoinedRoom, this._joinedRoomMethod);
652
562
  }
653
563
 
654
- /**
655
- * Notifies other users about spectating target changes
656
- * @param target The new target being spectated
657
- * @param _prevId The previous target's user ID
658
- */
659
564
  onSpectatedObjectChanged(target: PlayerView | undefined, _prevId?: string) {
660
565
  if (debug)
661
566
  console.log(this.context.connection.connectionId, "onSpectatedObjectChanged", target, _prevId);
@@ -667,10 +572,6 @@
667
572
  }
668
573
  }
669
574
 
670
- /**
671
- * Requests other users to follow this player or stop following
672
- * @param stop Whether to request users to stop following
673
- */
674
575
  onRequestFollowMe(stop: boolean = false) {
675
576
  if (debug)
676
577
  console.log("Request follow", this.context.connection.connectionId);
@@ -682,19 +583,12 @@
682
583
  }
683
584
  }
684
585
 
685
- /**
686
- * Handles room join events
687
- */
688
586
  private onUserJoinedRoom() {
689
587
  if (getParam("followme")) {
690
588
  this.onRequestFollowMe();
691
589
  }
692
590
  }
693
591
 
694
- /**
695
- * Processes follower status change events from the network
696
- * @param evt The follower change event data
697
- */
698
592
  private onFollowerEvent(evt: SpectatorFollowerChangedEventModel) {
699
593
  const userBeingFollowed = evt.targetUserId;
700
594
  const userThatIsFollowing = evt.guid;
@@ -721,9 +615,6 @@
721
615
  }
722
616
  }
723
617
 
724
- /**
725
- * Removes followers that are no longer connected to the room
726
- */
727
618
  private removeDisconnectedFollowers() {
728
619
  for (let i = this.followers.length - 1; i >= 0; i--) {
729
620
  const id = this.followers[i];
@@ -735,11 +626,6 @@
735
626
 
736
627
  private _lastRequestFollowUser: SpectatorFollowEventModel | undefined;
737
628
 
738
- /**
739
- * Handles follow requests from other users
740
- * @param evt The follow request event
741
- * @returns True if the request was handled successfully
742
- */
743
629
  private onRequestFollowEvent(evt: SpectatorFollowEventModel) {
744
630
  this._lastRequestFollowUser = evt;
745
631
 
@@ -766,10 +652,6 @@
766
652
  }
767
653
 
768
654
  private _enforceFollowInterval: any;
769
-
770
- /**
771
- * Periodically retries following a user if the initial attempt failed
772
- */
773
655
  private enforceFollow() {
774
656
  if (this._enforceFollowInterval) return;
775
657
  this._enforceFollowInterval = setInterval(() => {
src/engine-components/SpriteRenderer.ts CHANGED
@@ -155,11 +155,7 @@
155
155
  export class SpriteSheet {
156
156
 
157
157
  @serializable(Sprite)
158
- sprites: Sprite[];
159
-
160
- constructor() {
161
- this.sprites = [];
162
- }
158
+ sprites!: Sprite[];
163
159
  }
164
160
 
165
161
  export class SpriteData {
@@ -175,13 +171,6 @@
175
171
  // hence the spriteSheet field is undefined by default
176
172
  constructor() { }
177
173
 
178
- clone() {
179
- const i = new SpriteData();
180
- i.index = this.index;
181
- i.spriteSheet = this.spriteSheet;
182
- return i;
183
- }
184
-
185
174
  /**
186
175
  * Set the sprite to be rendered in the currently assigned sprite sheet at the currently active index {@link index}
187
176
  */
@@ -331,6 +320,7 @@
331
320
  if (typeof value === "number") {
332
321
  const index = Math.floor(value);
333
322
  this.spriteIndex = index;
323
+ return;
334
324
  }
335
325
  else if (value instanceof Sprite) {
336
326
  if (!this._spriteSheet) {
@@ -338,8 +328,8 @@
338
328
  }
339
329
  if (this._spriteSheet.sprite != value) {
340
330
  this._spriteSheet.sprite = value;
331
+ this.updateSprite();
341
332
  }
342
- this.updateSprite();
343
333
  }
344
334
  else if (value != this._spriteSheet) {
345
335
  this._spriteSheet = value;
@@ -352,6 +342,7 @@
352
342
  */
353
343
  set spriteIndex(value: number) {
354
344
  if (!this._spriteSheet) return;
345
+ if (value === this.spriteIndex) return;
355
346
  this._spriteSheet.index = value;
356
347
  this.updateSprite();
357
348
  }
@@ -368,19 +359,13 @@
368
359
  private _spriteSheet?: SpriteData;
369
360
  private _currentSprite?: Mesh;
370
361
 
371
-
372
362
  /** @internal */
373
363
  awake(): void {
374
364
  this._currentSprite = undefined;
375
-
376
365
  if (!this._spriteSheet) {
377
- this._spriteSheet = SpriteData.create();
366
+ this._spriteSheet = new SpriteData();
367
+ this._spriteSheet.spriteSheet = new SpriteSheet();
378
368
  }
379
- else {
380
- // Ensure each SpriteRenderer has a unique spritesheet instance for cases where sprite renderer are cloned at runtime and then different sprites are assigned to each instance
381
- this._spriteSheet = this._spriteSheet.clone();
382
- }
383
-
384
369
  if (debug) {
385
370
  console.log("Awake", this.name, this, this.sprite);
386
371
  }
@@ -423,7 +408,6 @@
423
408
  }
424
409
  mat.transparent = true;
425
410
  mat.toneMapped = this.toneMapped;
426
- mat.depthWrite = false;
427
411
 
428
412
  if (sprite.texture && !mat.wireframe) {
429
413
  let tex = sprite.texture;
src/engine-components/SyncedTransform.ts CHANGED
@@ -19,12 +19,7 @@
19
19
 
20
20
  const builder = new flatbuffers.Builder();
21
21
 
22
- /**
23
- * Creates a flatbuffer model containing the transform data of a game object. Used by {@link SyncedTransform}
24
- * @param guid The unique identifier of the object to sync
25
- * @param b The behavior component containing transform data
26
- * @param fast Whether to use fast mode synchronization (syncs more frequently)
27
- * @returns A Uint8Array containing the serialized transform data
22
+ /** Creates a flatbuffer model containing the transform data of a game object. Used by {@link SyncedTransform}
28
23
  */
29
24
  export function createTransformModel(guid: string, b: Behaviour, fast: boolean = true): Uint8Array {
30
25
  builder.clear();
@@ -55,28 +50,18 @@
55
50
  })
56
51
 
57
52
  /**
58
- * SyncedTransform synchronizes the position and rotation of a game object over the network.
59
- * It handles ownership transfer, interpolation, and network state updates automatically.
53
+ * SyncedTransform is a behaviour that syncs the transform of a game object over the network.
60
54
  * @category Networking
61
55
  * @group Components
62
56
  */
63
57
  export class SyncedTransform extends Behaviour {
64
58
 
65
-
59
+
66
60
  // public autoOwnership: boolean = true;
67
- /** When true, overrides physics behavior when this object is owned by the local user */
68
61
  public overridePhysics: boolean = true
69
-
70
- /** Whether to smoothly interpolate position changes when receiving updates */
71
62
  public interpolatePosition: boolean = true;
72
-
73
- /** Whether to smoothly interpolate rotation changes when receiving updates */
74
63
  public interpolateRotation: boolean = true;
75
-
76
- /** When true, sends updates at a higher frequency, useful for fast-moving objects */
77
64
  public fastMode: boolean = false;
78
-
79
- /** When true, notifies other clients when this object is destroyed */
80
65
  public syncDestroy: boolean = false;
81
66
 
82
67
  // private _state!: SyncedTransformModel;
@@ -92,10 +77,7 @@
92
77
  private _receivedFastUpdate: boolean = false;
93
78
  private _shouldRequestOwnership: boolean = false;
94
79
 
95
- /**
96
- * Requests ownership of this object on the network.
97
- * You need to be connected to a room for this to work.
98
- */
80
+ /** Request ownership of an object - you need to be connected to a room */
99
81
  public requestOwnership() {
100
82
  if (debug)
101
83
  console.log("Request ownership");
@@ -107,18 +89,10 @@
107
89
  this._model.requestOwnership();
108
90
  }
109
91
 
110
- /**
111
- * Checks if this client has ownership of the object
112
- * @returns true if this client has ownership, false if not, undefined if ownership state is unknown
113
- */
114
92
  public hasOwnership(): boolean | undefined {
115
93
  return this._model?.hasOwnership ?? undefined;
116
94
  }
117
95
 
118
- /**
119
- * Checks if the object is owned by any client
120
- * @returns true if the object is owned, false if not, undefined if ownership state is unknown
121
- */
122
96
  public isOwned(): boolean | undefined {
123
97
  return this._model?.isOwned;
124
98
  }
@@ -166,9 +140,6 @@
166
140
  this.context.connection.stopListenBinary(SyncedTransformIdentifier, this.receivedDataCallback);
167
141
  }
168
142
 
169
- /**
170
- * Attempts to retrieve and apply the last known network state for this transform
171
- */
172
143
  private tryGetLastState() {
173
144
  const model = this.context.connection.tryGetState(this.guid) as unknown as SyncedTransformModel;
174
145
  if (model) this.onReceivedData(model);
@@ -176,10 +147,6 @@
176
147
 
177
148
  private tempEuler: Euler = new Euler();
178
149
 
179
- /**
180
- * Handles incoming network data for this transform
181
- * @param data The model containing transform information
182
- */
183
150
  private onReceivedData(data: SyncedTransformModel) {
184
151
  if (this.destroyed) return;
185
152
  if (typeof data.guid === "function" && data.guid() === this.guid) {
@@ -216,10 +183,7 @@
216
183
  }
217
184
  }
218
185
 
219
- /**
220
- * @internal
221
- * Initializes tracking of position and rotation when component is enabled
222
- */
186
+ /** @internal */
223
187
  onEnable(): void {
224
188
  this.lastWorldPos.copy(this.worldPosition);
225
189
  this.lastWorldRotation.copy(this.worldQuaternion);
@@ -230,10 +194,7 @@
230
194
  }
231
195
  }
232
196
 
233
- /**
234
- * @internal
235
- * Releases ownership when component is disabled
236
- */
197
+ /** @internal */
237
198
  onDisable(): void {
238
199
  if (this._model)
239
200
  this._model.freeOwnership();
@@ -244,11 +205,7 @@
244
205
  private lastWorldPos!: Vector3;
245
206
  private lastWorldRotation!: Quaternion;
246
207
 
247
- /**
248
- * @internal
249
- * Handles transform synchronization before each render frame
250
- * Sends updates when owner, receives and applies updates when not owner
251
- */
208
+ /** @internal */
252
209
  onBeforeRender() {
253
210
  if (!this.activeAndEnabled || !this.context.connection.isConnected) return;
254
211
  // console.log("BEFORE RENDER", this.destroyed, this.guid, this._model?.isOwned, this.name, this.gameObject);
src/engine-components/TransformGizmo.ts CHANGED
@@ -8,43 +8,27 @@
8
8
  import { SyncedTransform } from "./SyncedTransform.js";
9
9
 
10
10
  /**
11
- * TransformGizmo displays manipulation controls for translating, rotating, and scaling objects in the scene.
12
- * It wraps three.js {@link TransformControls} and provides keyboard shortcuts for changing modes and settings.
11
+ * TransformGizmo is a component that displays a gizmo for transforming the object in the scene.
13
12
  * @category Helpers
14
13
  * @group Components
15
14
  */
16
15
  export class TransformGizmo extends Behaviour {
17
16
 
18
- /**
19
- * When true, this is considered a helper gizmo and will only be shown if showGizmos is enabled in engine parameters.
20
- */
21
17
  @serializable()
22
18
  public isGizmo: boolean = false;
23
19
 
24
- /**
25
- * Specifies the translation grid snap value in world units.
26
- * Applied when holding Shift while translating an object.
27
- */
28
20
  @serializable()
29
21
  public translationSnap: number = 1;
30
22
 
31
- /**
32
- * Specifies the rotation snap angle in degrees.
33
- * Applied when holding Shift while rotating an object.
34
- */
35
23
  @serializable()
36
24
  public rotationSnapAngle: number = 15;
37
25
 
38
- /**
39
- * Specifies the scale snapping value.
40
- * Applied when holding Shift while scaling an object.
41
- */
42
26
  @serializable()
43
27
  public scaleSnap: number = .25;
44
28
 
45
29
  /**
46
- * Gets the underlying three.js {@link TransformControls} instance.
47
- * @returns The TransformControls instance or undefined if not initialized.
30
+ * Get the underlying three.js TransformControls instance.
31
+ * @returns The TransformControls instance.
48
32
  */
49
33
  get control() {
50
34
  return this._control;
@@ -97,10 +81,6 @@
97
81
  window.removeEventListener('keyup', this.windowKeyUpListener);
98
82
  }
99
83
 
100
- /**
101
- * Enables grid snapping for transform operations according to set snap values.
102
- * This applies the translationSnap, rotationSnapAngle, and scaleSnap properties to the controls.
103
- */
104
84
  enableSnapping() {
105
85
  if (this._control) {
106
86
  this._control.setTranslationSnap(this.translationSnap);
@@ -109,10 +89,6 @@
109
89
  }
110
90
  }
111
91
 
112
- /**
113
- * Disables grid snapping for transform operations.
114
- * Removes all snapping constraints from the transform controls.
115
- */
116
92
  disableSnapping() {
117
93
  if (this._control) {
118
94
  this._control.setTranslationSnap(null);
@@ -121,11 +97,6 @@
121
97
  }
122
98
  }
123
99
 
124
- /**
125
- * Event handler for when dragging state changes.
126
- * Disables orbit controls during dragging and requests ownership of the transform if it's synchronized.
127
- * @param event The drag change event
128
- */
129
100
  private onControlChangedEvent = (event) => {
130
101
  const orbit = this.orbit;
131
102
  if (orbit) orbit.enabled = !event.value;
@@ -138,18 +109,7 @@
138
109
  }
139
110
  }
140
111
 
141
- /**
142
- * Handles keyboard shortcuts for transform operations:
143
- * - Q: Toggle local/world space
144
- * - W: Translation mode
145
- * - E: Rotation mode
146
- * - R: Scale mode
147
- * - Shift: Enable snapping (while held)
148
- * - +/-: Adjust gizmo size
149
- * - X/Y/Z: Toggle visibility of respective axis
150
- * - Spacebar: Toggle controls enabled state
151
- * @param event The keyboard event
152
- */
112
+
153
113
  private windowKeyDownListener = (event) => {
154
114
  if (!this.enabled) return;
155
115
  if (!this._control) return;
@@ -202,11 +162,6 @@
202
162
  }
203
163
  }
204
164
 
205
- /**
206
- * Handles keyboard key release events.
207
- * Currently only handles releasing Shift key to disable snapping.
208
- * @param event The keyboard event
209
- */
210
165
  private windowKeyUpListener = (event) => {
211
166
  if (!this.enabled) return;
212
167
  switch (event.keyCode) {
src/engine-components/postprocessing/Volume.ts CHANGED
@@ -62,11 +62,11 @@
62
62
  * Add a post processing effect to the stack and schedules the effect stack to be re-created.
63
63
  */
64
64
  addEffect<T extends PostProcessingEffect | Effect>(effect: T): T {
65
+
65
66
  let entry = effect as PostProcessingEffect;
66
67
  if (!(entry instanceof PostProcessingEffect)) {
67
68
  entry = new EffectWrapper(entry);
68
69
  }
69
- if(entry.gameObject === undefined) this.gameObject.addComponent(entry);
70
70
  if (this._effects.includes(entry)) return effect;
71
71
  this._effects.push(entry);
72
72
  this._isDirty = true;
@@ -125,7 +125,7 @@
125
125
  }
126
126
 
127
127
  // ensure the profile is initialized
128
- this.sharedProfile?.__init(this);
128
+ this.sharedProfile?.init();
129
129
  }
130
130
 
131
131
  onEnable(): void {
@@ -190,7 +190,7 @@
190
190
  private _isDirty: boolean = false;
191
191
 
192
192
  private apply() {
193
- if (debug) console.log(`Apply PostProcessing "${this.name}"`);
193
+ if (debug) console.log("Apply PostProcessing " + this.name);
194
194
 
195
195
  if (isDevEnvironment()) {
196
196
  if (this._lastApplyTime !== undefined && Date.now() - this._lastApplyTime < 100) {
@@ -209,16 +209,18 @@
209
209
  if (this.sharedProfile?.components) {
210
210
  const comps = this.sharedProfile.components;
211
211
  for (const effect of comps) {
212
- if (effect.active && effect.enabled && !this._activeEffects.includes(effect))
212
+ if (effect.active && !this._activeEffects.includes(effect))
213
213
  this._activeEffects.push(effect);
214
214
  }
215
215
  }
216
216
  // add effects registered via code
217
217
  for (const effect of this._effects) {
218
- if (effect.active && effect.enabled && !this._activeEffects.includes(effect))
218
+ if (effect.active && !this._activeEffects.includes(effect))
219
219
  this._activeEffects.push(effect);
220
220
  }
221
221
 
222
+ if (debug) console.log("Apply PostProcessing", this._activeEffects);
223
+
222
224
  if (this._activeEffects.length > 0) {
223
225
  if (!this._postprocessing)
224
226
  this._postprocessing = new PostProcessingHandler(this.context);
src/engine-components/postprocessing/VolumeProfile.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  import { serializeable } from "../../engine/engine_serialization_decorator.js";
2
2
  import { getParam } from "../../engine/engine_utils.js";
3
- import { Component } from "../Component.js";
4
3
  import { PostProcessingEffect } from "./PostProcessingEffect.js";
5
- import type { Volume } from "./Volume.js";
6
4
 
7
5
  const debug = getParam("debugpost");
8
6
 
@@ -40,14 +38,8 @@
40
38
  * call init on all components
41
39
  * @hidden
42
40
  **/
43
- __init(owner: Component) {
44
- this.components?.forEach(c => {
45
- // Make sure all components are added to the gameobject (this is not the case when e.g. effects are serialized from Unity)
46
- if (c.gameObject === undefined) {
47
- owner.gameObject.addComponent(c);
48
- }
49
- c.init()
50
- });
41
+ init() {
42
+ this.components?.forEach(c => c.init());
51
43
  }
52
44
 
53
45
  addEffect(effect: PostProcessingEffect) {
src/engine-components/webxr/WebARSessionRoot.ts CHANGED
@@ -49,25 +49,18 @@
49
49
  }
50
50
  }
51
51
 
52
- private static _hasPlaced: boolean = false;
53
- /**
54
- * @returns true if the scene has been placed in AR by the user or automatic placement
55
- */
56
- static get hasPlaced(): boolean {
57
- return this._hasPlaced;
58
- }
59
52
 
60
-
61
- /** The scale of the user in AR.
62
- * **NOTE**: a large value makes the scene appear smaller
53
+ /** The scale of a user in AR:
54
+ * Note: a large value makes the scene appear smaller
63
55
  * @default 1
64
56
  */
65
57
  get arScale(): number {
66
58
  return this._arScale;
67
59
  }
68
60
  set arScale(val: number) {
69
- this._arScale = Math.max(0.000001, val);
70
- this.onSetScale();
61
+ if (val === this._arScale) return;
62
+ this._arScale = val;
63
+ this.onScaleChanged();
71
64
  }
72
65
  private _arScale: number = 1;
73
66
 
@@ -141,7 +134,6 @@
141
134
  if (debug) console.log("ENTER WEBXR: SessionRoot start...");
142
135
 
143
136
  this._anchor = null;
144
- WebARSessionRoot._hasPlaced = false;
145
137
 
146
138
  // if (_args.xr.session.enabledFeatures?.includes("image-tracking")) {
147
139
  // console.warn("Image tracking is enabled - will not place scene");
@@ -201,7 +193,6 @@
201
193
  this.onRevertSceneChanges();
202
194
  // this._anchor?.delete();
203
195
  this._anchor = null;
204
- WebARSessionRoot._hasPlaced = false;
205
196
  this._rigPlacementMatrix = undefined;
206
197
  }
207
198
  onUpdateXR(args: NeedleXREventArgs): void {
@@ -414,7 +405,6 @@
414
405
  reticle.quaternion.copy(reticle["lastQuat"]);
415
406
 
416
407
  this.onApplyPose(reticle);
417
- WebARSessionRoot._hasPlaced = true;
418
408
 
419
409
  if (this.useXRAnchor) {
420
410
  this.onCreateAnchor(NeedleXRSession.active!, hit);
@@ -427,16 +417,8 @@
427
417
  }
428
418
  }
429
419
 
430
- private onSetScale() {
431
- if (!WebARSessionRoot._hasPlaced) return;
432
- const rig = NeedleXRSession.active?.rig?.gameObject;
433
- if (rig) {
434
- const currentScale = NeedleXRSession.active?.rigScale || 1;
435
- const newScale = (1 / this._arScale) * currentScale;
436
- const scaleMatrix = new Matrix4().makeScale(newScale, newScale, newScale).invert();
437
- rig.matrix.premultiply(scaleMatrix);
438
- rig.matrix.decompose(rig.position, rig.quaternion, rig.scale);
439
- }
420
+ private onScaleChanged() {
421
+ // TODO: implement
440
422
  }
441
423
 
442
424
  private onRevertSceneChanges() {
@@ -534,7 +516,7 @@
534
516
  console.warn("No rig object to place");
535
517
  return;
536
518
  }
537
- // const rigScale = NeedleXRSession.active?.rigScale || 1;
519
+ const rigScale = NeedleXRSession.active?.rigScale || 1;
538
520
 
539
521
  // save the previous rig parent
540
522
  const previousParent = rigObject.parent || this.context.scene;
@@ -596,10 +578,6 @@
596
578
  private _scale: number = 1;
597
579
  private _hasChanged: boolean = false;
598
580
 
599
- get scale() {
600
- return this._scale;
601
- }
602
-
603
581
  // readonly translate: Vector3 = new Vector3();
604
582
  // readonly rotation: Quaternion = new Quaternion();
605
583
  // readonly scale: Vector3 = new Vector3(1, 1, 1);
@@ -616,7 +594,6 @@
616
594
  reset() {
617
595
  this._scale = 1;
618
596
  this.offset.identity();
619
- this._hasChanged = true;
620
597
  }
621
598
  get hasChanged() { return this._hasChanged; }
622
599
 
src/engine-components/webxr/WebXR.ts CHANGED
@@ -31,127 +31,74 @@
31
31
  export class WebXR extends Behaviour {
32
32
 
33
33
  // UI
34
- /**
35
- * When enabled, a button will be automatically added to {@link NeedleMenu} that allows users to enter VR mode.
36
- */
34
+ /** When enabled a button will be added to the UI to enter VR */
37
35
  @serializable()
38
36
  createVRButton: boolean = true;
39
-
40
- /**
41
- * When enabled, a button will be automatically added to {@link NeedleMenu} that allows users to enter AR mode.
42
- */
37
+ /** When enabled a button will be added to the UI to enter AR */
43
38
  @serializable()
44
39
  createARButton: boolean = true;
45
-
46
- /**
47
- * When enabled, a button to send the experience to an Oculus Quest will be shown if the current device does not support VR.
48
- * This helps direct users to compatible devices for optimal VR experiences.
49
- */
40
+ /** When enabled a send to quest button will be shown if the device does not support VR */
50
41
  @serializable()
51
42
  createSendToQuestButton: boolean = true;
52
-
53
- /**
54
- * When enabled, a QR code will be generated and displayed on desktop devices to allow easy opening of the experience on mobile devices.
55
- */
43
+ /** When enabled a QRCode will be created to open the website on a mobile device */
56
44
  @serializable()
57
45
  createQRCode: boolean = true;
58
46
 
59
47
  // VR Settings
60
- /**
61
- * When enabled, default movement controls will be automatically added to the scene when entering VR.
62
- * This includes teleportation and smooth locomotion options for VR controllers.
63
- */
48
+ /** When enabled default movement behaviour will be added */
64
49
  @serializable()
65
50
  useDefaultControls: boolean = true;
66
-
67
- /**
68
- * When enabled, 3D models representing the user's VR controllers will be automatically created and rendered in the scene.
69
- */
51
+ /** When enabled controller models will automatically be created and updated when you are using controllers in WebXR */
70
52
  @serializable()
71
53
  showControllerModels: boolean = true;
72
-
73
- /**
74
- * When enabled, 3D models representing the user's hands will be automatically created and rendered when hand tracking is available.
75
- */
54
+ /** When enabled hand models will automatically be created and updated when you are using hands in WebXR */
76
55
  @serializable()
77
56
  showHandModels: boolean = true;
78
57
 
79
58
  // AR Settings
80
- /**
81
- * When enabled, a reticle will be displayed to help place the scene in AR. The user must tap on a detected surface to position the scene.
82
- */
59
+ /** When enabled the scene must be placed in AR */
83
60
  @serializable()
84
61
  usePlacementReticle: boolean = true;
85
-
86
- /**
87
- * Optional custom 3D object to use as the AR placement reticle instead of the default one.
88
- */
62
+ /** When assigned this object will be used as the AR placement reticle */
89
63
  @serializable(AssetReference)
90
64
  customARPlacementReticle?: AssetReference;
91
-
92
- /**
93
- * When enabled, users can adjust the position, rotation, and scale of the AR scene with one or two fingers after initial placement.
94
- */
65
+ /** When enabled you can position, rotate or scale your AR scene with one or two fingers */
95
66
  @serializable()
96
67
  usePlacementAdjustment: boolean = true;
97
-
98
- /**
99
- * Determines the scale of the user relative to the scene in AR. Larger values make the 3D content appear smaller.
100
- * Only applies when `usePlacementReticle` is enabled.
101
- */
68
+ /** Used when `usePlacementReticle` is enabled. This is the scale of the user in the scene in AR. Larger values make the 3D content appear smaller */
102
69
  @serializable()
103
70
  arScale: number = 1;
104
-
105
- /**
106
- * When enabled, an XRAnchor will be created for the AR scene and its position will be regularly updated to match the anchor.
107
- * This can help with spatial persistence in AR experiences.
108
- * @experimental
109
- */
71
+ /** Experimental: When enabled an XRAnchor will be created for the AR scene and the position will be updated to the anchor position every few frames */
110
72
  @serializable()
111
73
  useXRAnchor: boolean = false;
112
-
113
74
  /**
114
- * When enabled, the scene will be automatically placed as soon as a suitable surface is detected in AR,
115
- * without requiring the user to tap to confirm placement.
75
+ * When enabled the scene will be placed automatically when a point in the real world is found
116
76
  */
117
77
  @serializable()
118
- autoPlace: boolean = false;
119
-
120
- /**
121
- * When enabled, the AR session root center will be automatically adjusted to place the center of the scene.
122
- * This helps ensure the scene is properly aligned with detected surfaces.
123
- */
78
+ autoPlace: boolean = true;
79
+ /** When enabled the AR session root center will be automatically adjusted to place the center of the scene */
124
80
  @serializable()
125
81
  autoCenter: boolean = false;
126
82
 
127
- /**
128
- * When enabled, a USDZExporter component will be automatically added to the scene if none is found.
129
- * This allows iOS and visionOS devices to view 3D content using Apple's AR QuickLook.
130
- */
83
+ /** When enabled a USDZExporter component will be added to the scene (if none is found) */
131
84
  @serializable()
132
85
  useQuicklookExport: boolean = false;
133
86
 
134
- /**
135
- * When enabled, the 'depth-sensing' WebXR feature will be requested to provide real-time depth occlusion.
136
- * Currently only supported on Oculus Quest devices.
137
- * @see https://developer.mozilla.org/en-US/docs/Web/API/XRDepthInformation
138
- * @experimental
87
+
88
+ /** Preview feature enabling occlusion (when available: https://github.com/cabanier/three.js/commit/b6ee92bcd8f20718c186120b7f19a3b68a1d4e47)
89
+ * Enables the 'depth-sensing' WebXR feature to provide realtime depth occlusion. Only supported on Oculus Quest right now.
139
90
  */
140
91
  @serializable()
141
92
  useDepthSensing: boolean = false;
142
93
 
143
94
  /**
144
- * When enabled, a {@link SpatialGrabRaycaster} will be added or enabled in the scene,
145
- * allowing users to interact with objects at a distance in VR/AR.
95
+ * When enabled the spatial grab raycaster will be added or enabled in the scene
146
96
  * @default true
147
97
  */
148
98
  @serializable()
149
99
  useSpatialGrab: boolean = true;
150
100
 
151
- /**
152
- * Specifies the avatar representation that will be created when entering a WebXR session.
153
- * Can be a reference to a 3D model or a boolean to use the default avatar.
154
- */
101
+ /** This avatar representation will be spawned when you enter a webxr session */
155
102
  @serializable(AssetReference)
156
103
  defaultAvatar?: AssetReference | boolean;
157
104
 
@@ -164,19 +111,10 @@
164
111
 
165
112
  static activeWebXRComponent: WebXR | null = null;
166
113
 
167
- /**
168
- * Initializes the WebXR component by obtaining the XR sync object for this context.
169
- * @internal
170
- */
171
114
  awake() {
172
115
  NeedleXRSession.getXRSync(this.context);
173
116
  }
174
117
 
175
- /**
176
- * Sets up the WebXR component when it's enabled. Checks for HTTPS connection,
177
- * sets up USDZ export if enabled, creates UI buttons, and configures avatar settings.
178
- * @internal
179
- */
180
118
  onEnable(): void {
181
119
  // check if we're on a secure connection:
182
120
  if (window.location.protocol !== "https:") {
@@ -215,21 +153,11 @@
215
153
  }
216
154
  }
217
155
 
218
- /**
219
- * Cleans up resources when the component is disabled.
220
- * Destroys the USDZ exporter if one was created and removes UI buttons.
221
- * @internal
222
- */
223
156
  onDisable(): void {
224
157
  this._usdzExporter?.destroy();
225
158
  this.removeButtons();
226
159
  }
227
160
 
228
- /**
229
- * Checks if WebXR is supported and offers an appropriate session.
230
- * This is used to show the WebXR session joining prompt in browsers that support it.
231
- * @returns A Promise that resolves to true if a session was offered, false otherwise
232
- */
233
161
  private async handleOfferSession() {
234
162
  if (this.createVRButton) {
235
163
  const hasVRSupport = await NeedleXRSession.isVRSupported();
@@ -254,32 +182,16 @@
254
182
  get sessionMode(): XRSessionMode | null {
255
183
  return NeedleXRSession.activeMode ?? null;;
256
184
  }
257
- /** While AR: this will return the currently active WebARSessionRoot component.
258
- * You can also query this component in your scene with `findObjectOfType(WebARSessionRoot)`
259
- */
260
- get arSessionRoot() {
261
- return this._activeWebARSessionRoot;
262
- }
263
185
 
264
- /** Call to start an WebVR session.
265
- *
266
- * This is a shorthand for `NeedleXRSession.start("immersive-vr", init, this.context)`
267
- */
186
+ /** Call to start an WebVR session */
268
187
  async enterVR(init?: XRSessionInit): Promise<NeedleXRSession | null> {
269
188
  return NeedleXRSession.start("immersive-vr", init, this.context);
270
189
  }
271
- /** Call to start an WebAR session
272
- *
273
- * This is a shorthand for `NeedleXRSession.start("immersive-ar", init, this.context)`
274
- */
190
+ /** Call to start an WebAR session */
275
191
  async enterAR(init?: XRSessionInit): Promise<NeedleXRSession | null> {
276
192
  return NeedleXRSession.start("immersive-ar", init, this.context);
277
193
  }
278
-
279
- /** Call to end a WebXR (AR or VR) session.
280
- *
281
- * This is a shorthand for `NeedleXRSession.stop()`
282
- */
194
+ /** Call to end a WebXR (AR or VR) session */
283
195
  exitXR() {
284
196
  NeedleXRSession.stop();
285
197
  }
@@ -287,18 +199,11 @@
287
199
  private _exitXRMenuButton?: HTMLElement;
288
200
  private _previousXRState: number = 0;
289
201
  private _spatialGrabRaycaster?: SpatialGrabRaycaster;
290
- private _activeWebARSessionRoot: WebARSessionRoot | null = null;
291
202
 
292
203
  private get isActiveWebXR() {
293
204
  return !WebXR.activeWebXRComponent || WebXR.activeWebXRComponent === this;
294
205
  }
295
206
 
296
- /**
297
- * Called before entering a WebXR session. Sets up optional features like depth sensing, if needed.
298
- * @param _mode The XR session mode being requested (immersive-ar or immersive-vr)
299
- * @param args The XRSessionInit object that will be passed to the WebXR API
300
- * @internal
301
- */
302
207
  onBeforeXR(_mode: XRSessionMode, args: XRSessionInit): void {
303
208
  if (!this.isActiveWebXR) {
304
209
  console.warn(`WebXR: another WebXR component is already active (${WebXR.activeWebXRComponent?.name}). This is ignored: ${this.name}`);
@@ -312,12 +217,6 @@
312
217
  }
313
218
  }
314
219
 
315
- /**
316
- * Called when a WebXR session begins. Sets up the scene for XR by configuring controllers,
317
- * AR placement, and other features based on component settings.
318
- * @param args Event arguments containing information about the started XR session
319
- * @internal
320
- */
321
220
  async onEnterXR(args: NeedleXREventArgs) {
322
221
  if (!this.isActiveWebXR) return;
323
222
 
@@ -345,7 +244,6 @@
345
244
  }
346
245
  }
347
246
 
348
- this._activeWebARSessionRoot = sessionroot;
349
247
  if (sessionroot) {
350
248
  // sessionroot.enabled = this.usePlacementReticle; // < not sure if we want to disable the session root when placement reticle if OFF...
351
249
  sessionroot.customReticle = this.customARPlacementReticle;
@@ -390,12 +288,6 @@
390
288
  }
391
289
  }
392
290
 
393
- /**
394
- * Called every frame during an active WebXR session.
395
- * Updates components that depend on the current XR state.
396
- * @param _args Event arguments containing information about the current XR session frame
397
- * @internal
398
- */
399
291
  onUpdateXR(_args: NeedleXREventArgs): void {
400
292
  if (!this.isActiveWebXR) return;
401
293
  if (this._spatialGrabRaycaster) {
@@ -403,12 +295,6 @@
403
295
  }
404
296
  }
405
297
 
406
- /**
407
- * Called when a WebXR session ends. Restores pre-session state,
408
- * removes temporary components, and cleans up resources.
409
- * @param _ Event arguments containing information about the ended XR session
410
- * @internal
411
- */
412
298
  onLeaveXR(_: NeedleXREventArgs): void {
413
299
  this._exitXRMenuButton?.remove();
414
300
 
@@ -424,8 +310,6 @@
424
310
  }
425
311
  this._createdComponentsInSession.length = 0;
426
312
 
427
- this._activeWebARSessionRoot = null;
428
-
429
313
  this.handleOfferSession();
430
314
 
431
315
  delayForFrames(1).then(() => WebXR.activeWebXRComponent = null);
@@ -455,10 +339,8 @@
455
339
  return models;
456
340
  }
457
341
 
458
- /**
459
- * Creates and instantiates the user's avatar representation in the WebXR session.
460
- * @param xr The active session
461
- */
342
+
343
+
462
344
  protected async createLocalAvatar(xr: NeedleXRSession) {
463
345
  if (this._playerSync && xr.running && typeof this.defaultAvatar != "boolean") {
464
346
  this._playerSync.asset = this.defaultAvatar;
@@ -466,11 +348,6 @@
466
348
  }
467
349
  }
468
350
 
469
- /**
470
- * Event handler called when a player avatar is spawned.
471
- * Ensures the avatar has the necessary Avatar component.
472
- * @param instance The spawned avatar 3D object
473
- */
474
351
  private onAvatarSpawned = (instance: Object3D) => {
475
352
  // spawned webxr avatars must have a avatar component
476
353
  if (debug) console.log("WebXR.onAvatarSpawned", instance);
@@ -483,16 +360,13 @@
483
360
 
484
361
  // HTML UI
485
362
 
486
- /** @deprecated use {@link getButtonsFactory} or directly access {@link WebXRButtonFactory.getOrCreate} */
363
+ /** @deprecated use `getButtonsFactory()` or access `WebXRButtonFactory.getOrCreate()` directory */
487
364
  getButtonsContainer(): WebXRButtonFactory {
488
365
  return this.getButtonsFactory();
489
366
  }
490
367
 
491
- /**
492
- * Returns the WebXR button factory, creating one if it doesn't exist.
493
- * Use this to access and modify WebXR UI buttons.
494
- * @returns The WebXRButtonFactory instance
495
- */
368
+ /** Calling this function will get the Needle WebXR button factory (it will be created if it doesnt exist yet)
369
+ * @returns the Needle WebXR button factory */
496
370
  getButtonsFactory(): WebXRButtonFactory {
497
371
  if (!this._buttonFactory) {
498
372
  this._buttonFactory = WebXRButtonFactory.getOrCreate();
@@ -500,15 +374,8 @@
500
374
  return this._buttonFactory;
501
375
  }
502
376
 
503
- /**
504
- * Reference to the WebXR button factory used by this component.
505
- */
506
377
  private _buttonFactory?: WebXRButtonFactory;
507
378
 
508
- /**
509
- * Creates and sets up UI elements for WebXR interaction based on component settings
510
- * and device capabilities. Handles creating AR, VR, QuickLook buttons and utility buttons like QR codes.
511
- */
512
379
  private handleCreatingHTML() {
513
380
  const xrButtonsPriority = 50;
514
381
 
@@ -556,25 +423,14 @@
556
423
  }
557
424
  }
558
425
 
559
- /**
560
- * Storage for UI buttons created by this component.
561
- */
562
426
  private readonly _buttons: HTMLElement[] = [];
563
427
 
564
- /**
565
- * Adds a button to the UI with the specified priority.
566
- * @param button The HTML element to add
567
- * @param priority The button's priority value (lower numbers appear first)
568
- */
569
428
  private addButton(button: HTMLElement, priority: number) {
570
429
  this._buttons.push(button);
571
430
  button.setAttribute("priority", priority.toString());
572
431
  this.context.menu.appendChild(button);
573
432
  }
574
433
 
575
- /**
576
- * Removes all buttons created by this component from the UI.
577
- */
578
434
  private removeButtons() {
579
435
  for (const button of this._buttons) {
580
436
  button.remove();