Needle Engine

Changes between version 3.36.3-beta.1 and 3.36.4-beta
Files changed (14) hide show
  1. src/engine/engine_addressables.ts +36 -4
  2. src/engine/engine_input.ts +5 -1
  3. src/engine/engine_instancing.ts +3 -0
  4. src/engine/engine_license.ts +4 -0
  5. src/engine/engine_networking_instantiate.ts +2 -0
  6. src/engine/engine_scenetools.ts +17 -0
  7. src/engine/engine_shaders.ts +24 -0
  8. src/engine/engine_texture.ts +3 -0
  9. src/engine/engine_three_utils.ts +56 -7
  10. src/engine/engine_types.ts +8 -1
  11. src/engine/engine_util_decorator.ts +3 -2
  12. src/engine/engine_utils.ts +3 -1
  13. src/engine-components/OrbitControls.ts +7 -0
  14. src/engine-components/SceneSwitcher.ts +70 -1
src/engine/engine_addressables.ts CHANGED
@@ -45,11 +45,17 @@
45
45
  }
46
46
 
47
47
 
48
- findAssetReference(key: string): AssetReference | null {
49
- return this._assetReferences[key] || null;
48
+ /**
49
+ * Find a registered AssetReference by its URL
50
+ */
51
+ findAssetReference(url: string): AssetReference | null {
52
+ return this._assetReferences[url] || null;
50
53
  }
51
54
 
52
- /** @internal */
55
+ /**
56
+ * Register an asset reference
57
+ * @internal
58
+ */
53
59
  registerAssetReference(ref: AssetReference): AssetReference {
54
60
  if (!ref.uri) return ref;
55
61
  if (!this._assetReferences[ref.uri]) {
@@ -83,8 +89,32 @@
83
89
  */
84
90
  export class AssetReference {
85
91
 
92
+ /**
93
+ * Experimental!
94
+ * @internal
95
+ * Get an AssetReference for a URL to be easily loaded.
96
+ * AssetReferences are cached so calling this method multiple times with the same arguments will always return the same AssetReference.
97
+ * @param url The URL of the asset to load. The url can be relative or absolute.
98
+ * @param context The context to use for loading the asset
99
+ * @returns the AssetReference for the URL
100
+ */
101
+ static getOrCreateFromUrl(url: string, context?: Context): AssetReference {
102
+ if (!context) {
103
+ context = Context.Current;
104
+ if (!context)
105
+ throw new Error("Context is required when sourceId is a string. When you call this method from a component you can call it with \"getOrCreate(this, url)\" where \"this\" is the component.");
106
+ }
107
+ const addressables = context.addressables;
108
+ const existing = addressables.findAssetReference(url);
109
+ if (existing) return existing;
110
+ const ref = new AssetReference(url, context.hash);
111
+ addressables.registerAssetReference(ref);
112
+ return ref;
113
+ }
114
+
86
115
  /**
87
- * Get an AssetReference for a URL to be easily loaded. AssetReferences are cached so calling this method multiple times with the same arguments will always return the same AssetReference.
116
+ * Get an AssetReference for a URL to be easily loaded.
117
+ * AssetReferences are cached so calling this method multiple times with the same arguments will always return the same AssetReference.
88
118
  */
89
119
  static getOrCreate(sourceId: SourceIdentifier | IComponent, url: string, context?: Context): AssetReference {
90
120
 
@@ -515,6 +545,7 @@
515
545
  }
516
546
 
517
547
 
548
+ /** @internal */
518
549
  export class ImageReferenceSerializer extends TypeSerializer {
519
550
  constructor() {
520
551
  super([ImageReference], "ImageReferenceSerializer");
@@ -578,6 +609,7 @@
578
609
  }
579
610
 
580
611
 
612
+ /** @internal */
581
613
  export class FileReferenceSerializer extends TypeSerializer {
582
614
  constructor() {
583
615
  super([FileReference], "FileReferenceSerializer");
src/engine/engine_input.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Intersection,Matrix4, Object3D, Ray, Vector2, Vector3 } from 'three';
1
+ import { Intersection, Matrix4, Object3D, Ray, Vector2, Vector3 } from 'three';
2
2
 
3
3
  import { showBalloonMessage, showBalloonWarning } from './debug/debug.js';
4
4
  import { Context } from './engine_setup.js';
@@ -207,6 +207,9 @@
207
207
  signal?: AbortSignal;
208
208
  }
209
209
 
210
+ /**
211
+ * The input system is responsible for handling all input events like pointer events (mouse, touch, xr controllers) and keyboard events.
212
+ */
210
213
  export class Input implements IInput {
211
214
 
212
215
  /** This is a list of event listeners per event type (e.g. pointerdown, pointerup, keydown...). Each entry contains a priority and list of listeners.
@@ -649,6 +652,7 @@
649
652
  vec2.y = -((vec2.y - this.context.domY) / this.context.domHeight) * 2 + 1;
650
653
  }
651
654
 
655
+ /** @internal */
652
656
  constructor(context: Context) {
653
657
  this.context = context;
654
658
  this.context.post_render_callbacks.push(this.onEndOfFrame);
src/engine/engine_instancing.ts CHANGED
@@ -6,6 +6,9 @@
6
6
  export const $instancingRenderer = Symbol("instancingRenderer");
7
7
  export const $instancingAutoUpdateBounds = Symbol("instancingAutoUpdateBounds");
8
8
 
9
+ /**
10
+ * Utility class for accessing instancing related properties
11
+ */
9
12
  export class InstancingUtil {
10
13
 
11
14
  /** Is this object rendered using a InstancedMesh */
src/engine/engine_license.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  let NEEDLE_ENGINE_LICENSE_TYPE: string = "basic";
11
11
  if (debug) console.log("License Type: " + NEEDLE_ENGINE_LICENSE_TYPE)
12
12
 
13
+ /** @internal */
13
14
  export function hasProLicense() {
14
15
  switch (NEEDLE_ENGINE_LICENSE_TYPE) {
15
16
  case "pro":
@@ -19,6 +20,7 @@
19
20
  return false;
20
21
  }
21
22
 
23
+ /** @internal */
22
24
  export function hasIndieLicense() {
23
25
  switch (NEEDLE_ENGINE_LICENSE_TYPE) {
24
26
  case "indie":
@@ -27,11 +29,13 @@
27
29
  return false;
28
30
  }
29
31
 
32
+ /** @internal */
30
33
  export function hasCommercialLicense() {
31
34
  return hasProLicense() || hasIndieLicense();
32
35
  }
33
36
 
34
37
  const _licenseCheckResultChangedCallbacks: ((result: boolean) => void)[] = [];
38
+ /** @internal */
35
39
  export function onLicenseCheckResultChanged(cb: (result: boolean) => void) {
36
40
  if (hasProLicense() || hasIndieLicense())
37
41
  return cb(true);
src/engine/engine_networking_instantiate.ts CHANGED
@@ -151,6 +151,8 @@
151
151
 
152
152
  // when a file is instantiated via some server (e.g. via file drop) we also want to send the info where the file can be downloaded
153
153
  // doing it this route will ensure we have
154
+
155
+ /** @internal */
154
156
  export class HostData {
155
157
  filename: string;
156
158
  hash: string;
src/engine/engine_scenetools.ts CHANGED
@@ -15,6 +15,7 @@
15
15
  import { NEEDLE_components } from "./extensions/NEEDLE_components.js";
16
16
 
17
17
 
18
+ /** @internal */
18
19
  export class NeedleGltfLoader implements INeedleGltfLoader {
19
20
  createBuiltinComponents(context: Context, gltfId: string, gltf: any, seed: number | UIDProvider | null, extension?: NEEDLE_components | undefined) {
20
21
  return createBuiltinComponents(context, gltfId, gltf, seed, extension);
@@ -103,6 +104,13 @@
103
104
  return loader;
104
105
  }
105
106
 
107
+ /** Load a gltf file from a url. This is the core method used by Needle Engine to load gltf files. All known extensions are registered here.
108
+ * @param context The current context
109
+ * @param data The gltf data as string or ArrayBuffer
110
+ * @param path The path to the gltf file
111
+ * @param seed The seed for generating unique ids
112
+ * @returns The loaded gltf object
113
+ */
106
114
  export async function parseSync(context: Context, data: string | ArrayBuffer, path: string, seed: number | UIDProvider | null): Promise<GLTF | undefined> {
107
115
  if (typeof path !== "string") {
108
116
  console.warn("Parse gltf binary without path, this might lead to errors in resolving extensions. Please provide the source path of the gltf/glb file", path, typeof path);
@@ -153,6 +161,15 @@
153
161
  });
154
162
  }
155
163
 
164
+ /**
165
+ * Load a gltf file from a url. This is the core method used by Needle Engine to load gltf files. All known extensions are registered here.
166
+ * @param context The current context
167
+ * @param url The url to the gltf file
168
+ * @param sourceId The source id of the gltf file - this is usually the url
169
+ * @param seed The seed for generating unique ids
170
+ * @param prog A progress callback
171
+ * @returns The loaded gltf object
172
+ */
156
173
  export async function loadSync(context: Context, url: string, sourceId: string, seed: number | UIDProvider | null, prog?: (ProgressEvent) => void): Promise<GLTF | undefined> {
157
174
  // better to create new loaders every time
158
175
  // (maybe we can cache them...)
src/engine/engine_shaders.ts CHANGED
@@ -11,6 +11,12 @@
11
11
  white[0] = 255; white[1] = 255; white[2] = 255; white[3] = 255;
12
12
  export const whiteDefaultTexture = new DataTexture(white, 1, 1, RGBAFormat);
13
13
 
14
+ /**
15
+ * Creates a new texture with a single color
16
+ * @param col Color to use
17
+ * @param size Size of the texture
18
+ * @returns A texture with the specified color
19
+ */
14
20
  export function createFlatTexture(col: RGBAColor | Color, size: number = 1) {
15
21
  const hasAlpha = "alpha" in col;
16
22
  const length = size * size;
@@ -31,6 +37,15 @@
31
37
  return tex;
32
38
  }
33
39
 
40
+ /**
41
+ * Creates a new texture with three colors
42
+ * @param col0 First color
43
+ * @param col1 Second color
44
+ * @param col2 Third color
45
+ * @param width Width of the texture
46
+ * @param height Height of the texture
47
+ * @returns A texture with the specified colors
48
+ */
34
49
  export function createTrilightTexture<T extends Color>(col0: T, col1: T, col2: T, width: number = 1, height: number = 3) {
35
50
  const hasAlpha = false;// "alpha" in col0;
36
51
  const channels = 4;
@@ -62,11 +77,13 @@
62
77
  return tex;
63
78
  }
64
79
 
80
+ /** @internal */
65
81
  export enum Stage {
66
82
  Vertex,
67
83
  Fragment,
68
84
  }
69
85
 
86
+ /** @internal */
70
87
  export class UnityShaderStage {
71
88
  stage: Stage;
72
89
  code: string;
@@ -105,9 +122,11 @@
105
122
  }
106
123
  }
107
124
 
125
+ /** @internal */
108
126
  export const lib = new ShaderLib();
109
127
 
110
128
 
129
+ /** @internal */
111
130
  export function ToUnityMatrixArray(mat: Matrix4, buffer?: Array<Vector4>): Array<Vector4> {
112
131
  const arr = mat.elements;
113
132
  if (!buffer)
@@ -126,6 +145,8 @@
126
145
 
127
146
  const noAmbientLight: Array<number> = [];
128
147
  const copyBuffer: Array<number> = [];
148
+
149
+ /** @internal */
129
150
  export function SetUnitySphericalHarmonics(obj: object, array?: number[]) {
130
151
 
131
152
  if (noAmbientLight.length === 0) {
@@ -147,6 +168,7 @@
147
168
  obj["unity_SHC"] = { value: new Vector4(array[24], array[25], array[26], 1) };
148
169
  }
149
170
 
171
+ /** @internal */
150
172
  export class ShaderBundle {
151
173
  readonly vertexShader: string;
152
174
  readonly fragmentShader: string;
@@ -159,6 +181,7 @@
159
181
  }
160
182
  }
161
183
 
184
+ /** @internal */
162
185
  export async function FindShaderTechniques(shaderData: SHADERDATA.ShaderData, id: number): Promise<ShaderBundle | null> {
163
186
  // console.log(shaderData);
164
187
  if (!shaderData) {
@@ -191,6 +214,7 @@
191
214
  return null;
192
215
  }
193
216
 
217
+ /** @internal */
194
218
  async function loadShaderCode(shader: SHADERDATA.Shader) {
195
219
  const uri = shader.uri;
196
220
  if (!uri) return;
src/engine/engine_texture.ts CHANGED
@@ -21,6 +21,9 @@
21
21
  */
22
22
  export class RenderTexture extends WebGLRenderTarget {
23
23
 
24
+ /**
25
+ * Render the scene to the texture
26
+ */
24
27
  render(scene: Object3D, camera: Camera, renderer: WebGLRenderer | EffectComposer) {
25
28
  if (renderer instanceof EffectComposer) {
26
29
  if (!this["_unsupported_effectcomposer_warning"]) {
src/engine/engine_three_utils.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { AnimationAction, Euler, Mesh,Object3D, PerspectiveCamera, PlaneGeometry, Quaternion, Scene, Texture, Uniform, Vector3 } from "three";
2
- import { ShaderMaterial,WebGLRenderer } from "three";
1
+ import { AnimationAction, Euler, Mesh, Object3D, PerspectiveCamera, PlaneGeometry, Quaternion, Scene, Texture, Uniform, Vector3 } from "three";
2
+ import { ShaderMaterial, WebGLRenderer } from "three";
3
3
 
4
4
  import { Mathf } from "./engine_math.js"
5
5
  import { CircularBuffer } from "./engine_utils.js";
@@ -13,6 +13,7 @@
13
13
  }
14
14
 
15
15
  const flipYQuat: Quaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
16
+
16
17
  export function lookAtInverse(obj: Object3D, target: Vector3) {
17
18
 
18
19
  obj.lookAt(target);
@@ -50,6 +51,14 @@
50
51
 
51
52
 
52
53
  const _tempVecs = new CircularBuffer(() => new Vector3(), 100);
54
+
55
+ /** Gets a temporary vector. If a vector is passed in it will be copied to the temporary vector
56
+ * Temporary vectors are cached and reused internally. Don't store them!
57
+ * @param vecOrX the vector to copy or the x value
58
+ * @param y the y value
59
+ * @param z the z value
60
+ * @returns a temporary vector
61
+ */
53
62
  export function getTempVector(vecOrX?: Vector3 | number | DOMPointReadOnly, y?: number, z?: number) {
54
63
  const vec = _tempVecs.get();
55
64
  if (vecOrX instanceof Vector3) vec.copy(vecOrX);
@@ -62,6 +71,13 @@
62
71
  return vec;
63
72
  }
64
73
  const _tempQuats = new CircularBuffer(() => new Quaternion(), 100);
74
+
75
+ /**
76
+ * Gets a temporary quaternion. If a quaternion is passed in it will be copied to the temporary quaternion
77
+ * Temporary quaternions are cached and reused internally. Don't store them!
78
+ * @param value the quaternion to copy
79
+ * @returns a temporary quaternion
80
+ */
65
81
  export function getTempQuaternion(value?: Quaternion | DOMPointReadOnly) {
66
82
  const val = _tempQuats.get();
67
83
  if (value instanceof Quaternion) val.copy(value);
@@ -73,6 +89,13 @@
73
89
  const _worldPositions = new CircularBuffer(() => new Vector3(), 100);
74
90
  const _lastMatrixWorldUpdateKey = Symbol("lastMatrixWorldUpdateKey");
75
91
 
92
+ /**
93
+ * Get the world position of an object
94
+ * @param obj the object to get the world position from
95
+ * @param vec a vector to store the result in. If not passed in a temporary vector will be used
96
+ * @param updateParents if true the parents will be updated before getting the world position
97
+ * @returns the world position
98
+ */
76
99
  export function getWorldPosition(obj: Object3D, vec: Vector3 | null = null, updateParents: boolean = true): Vector3 {
77
100
  const wp = vec ?? _worldPositions.get();
78
101
  if (!obj) return wp.set(0, 0, 0);
@@ -87,20 +110,34 @@
87
110
  return wp;
88
111
  }
89
112
 
90
- export function setWorldPosition(obj: Object3D, val: Vector3) {
91
- if (!obj) return;
113
+ /**
114
+ * Set the world position of an object
115
+ * @param obj the object to set the world position of
116
+ * @param val the world position to set
117
+ */
118
+ export function setWorldPosition(obj: Object3D, val: Vector3): Object3D {
119
+ if (!obj) return obj;
92
120
  const wp = _worldPositions.get();
93
121
  if (val !== wp)
94
122
  wp.copy(val);
95
123
  const obj2 = obj?.parent ?? obj;
96
124
  obj2.worldToLocal(wp);
97
125
  obj.position.set(wp.x, wp.y, wp.z);
126
+ return obj;
98
127
  }
99
128
 
100
- export function setWorldPositionXYZ(obj: Object3D, x: number, y: number, z: number) {
129
+ /**
130
+ * Set the world position of an object
131
+ * @param obj the object to set the world position of
132
+ * @param x the x position
133
+ * @param y the y position
134
+ * @param z the z position
135
+ */
136
+ export function setWorldPositionXYZ(obj: Object3D, x: number, y: number, z: number): Object3D {
101
137
  const wp = _worldPositions.get();
102
138
  wp.set(x, y, z);
103
139
  setWorldPosition(obj, wp);
140
+ return obj;
104
141
  }
105
142
 
106
143
 
@@ -254,7 +291,7 @@
254
291
 
255
292
  export function getParentHierarchyPath(obj: Object3D): string {
256
293
  let path = obj?.name || "";
257
- if(!obj) return path;
294
+ if (!obj) return path;
258
295
  let parent = obj.parent;
259
296
  while (parent) {
260
297
  path = parent.name + "/" + path;
@@ -278,6 +315,9 @@
278
315
 
279
316
 
280
317
 
318
+ /**
319
+ * Utility class to perform various graphics operations like copying textures to canvas
320
+ */
281
321
  export class Graphics {
282
322
  private static planeGeometry = new PlaneGeometry(2, 2, 1, 1);
283
323
  private static renderer: WebGLRenderer | undefined = undefined;
@@ -300,6 +340,9 @@
300
340
  }`;
301
341
  private static blipMaterial: ShaderMaterial | undefined = undefined;
302
342
 
343
+ /**
344
+ * Create a blit material for copying textures
345
+ */
303
346
  static createBlitMaterial(fragment: string): ShaderMaterial {
304
347
  return new ShaderMaterial({
305
348
  uniforms: { map: new Uniform(null) },
@@ -309,7 +352,13 @@
309
352
  }
310
353
  private static mesh: Mesh | undefined = undefined;
311
354
 
312
- static copyTexture(texture: Texture, blitMaterial?: ShaderMaterial) {
355
+ /**
356
+ * Copy a texture to a new texture
357
+ * @param texture the texture to copy
358
+ * @param blitMaterial the material to use for copying (optional)
359
+ * @returns the newly created, copied texture
360
+ */
361
+ static copyTexture(texture: Texture, blitMaterial?: ShaderMaterial): Texture {
313
362
  const material = blitMaterial ?? this.blipMaterial!;
314
363
  material.uniforms.map.value = texture;
315
364
  material.needsUpdate = true;
src/engine/engine_types.ts CHANGED
@@ -320,6 +320,9 @@
320
320
 
321
321
  const contactsVectorBuffer = new CircularBuffer(() => new Vector3(), 20);
322
322
 
323
+ /**
324
+ * Holds information about physics contacts
325
+ */
323
326
  export class ContactPoint {
324
327
 
325
328
  private readonly _point: Vec3;
@@ -350,6 +353,7 @@
350
353
  return target.set(this._tangentVelocity.x, this._tangentVelocity.y, this._tangentVelocity.z);
351
354
  }
352
355
 
356
+ /** @internal */
353
357
  constructor(point: Vec3, dist: number, normal: Vec3, impulse: number, friction: number, tangentVelocity: Vec3) {
354
358
  this._point = point;
355
359
  this.distance = dist;
@@ -360,12 +364,15 @@
360
364
  }
361
365
  }
362
366
 
363
- /// all info in here must be readonly because the object is only created once per started collision
367
+ /**
368
+ * Holds information about a collision event. Includes a list of contact points and the colliders involved
369
+ */
364
370
  export class Collision {
365
371
 
366
372
  /** The contact points of this collision. Contains information about positions, normals, distance, friction, impulse... */
367
373
  readonly contacts: ContactPoint[];
368
374
 
375
+ /** @internal */
369
376
  constructor(obj: IGameObject, otherCollider: ICollider, contacts: ContactPoint[]) {
370
377
  this.me = obj;
371
378
  this._collider = otherCollider;
src/engine/engine_util_decorator.ts CHANGED
@@ -96,8 +96,9 @@
96
96
 
97
97
 
98
98
 
99
- /** experimental attribute - use to hook into another type's methods and run before the other methods run (similar to Harmony prefixes).
100
- * Return false to prevent the original method from running.
99
+ /** Experimental attribute
100
+ * Use to hook into another type's methods and run before the other methods run (similar to Harmony prefixes).
101
+ * Return false to prevent the original method from running.
101
102
  */
102
103
  export const prefix = function <T>(type: Constructor<T>) {
103
104
  return function (target: IComponent | any, _propertyKey: string | { name: string }, _PropertyDescriptor: PropertyDescriptor) {
src/engine/engine_utils.ts CHANGED
@@ -319,10 +319,12 @@
319
319
  if (pathIndex >= 0) {
320
320
  // Take the source uri as the base path
321
321
  const basePath = source.substring(0, pathIndex + 1);
322
+ // make sure we don't have double slashes
323
+ while (basePath.endsWith("/") && uri.startsWith("/")) uri = uri.substring(1);
322
324
  // Append the relative uri
323
325
  const newUri = basePath + uri;
324
326
  // newUri = new URL(newUri, globalThis.location.href).href;
325
- if (debugGetPath) console.log("source:", source, "- changed uri \nfrom", uri, "\n→ ", newUri, "\n" + basePath);
327
+ if (debugGetPath) console.log("source:", source, "changed uri \nfrom", uri, "\nto ", newUri, "\nbasePath: " + basePath);
326
328
  return newUri;
327
329
  }
328
330
  return uri;
src/engine-components/OrbitControls.ts CHANGED
@@ -639,10 +639,17 @@
639
639
  expandByObjectRecursive(child);
640
640
  }
641
641
  }
642
+ let hasAnyObject = false;
642
643
  for (const object of objects) {
644
+ if (!object) continue;
645
+ hasAnyObject = true;
643
646
  object.updateMatrixWorld();
644
647
  expandByObjectRecursive(object);
645
648
  }
649
+ if (!hasAnyObject) {
650
+ console.warn("No objects to fit camera to...");
651
+ return;
652
+ }
646
653
 
647
654
  camera.updateMatrixWorld();
648
655
  camera.updateProjectionMatrix();
src/engine-components/SceneSwitcher.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Object3D } from "three";
2
2
 
3
- import { AssetReference } from "../engine/engine_addressables.js";
3
+ import { Addressables, AssetReference } from "../engine/engine_addressables.js";
4
4
  import { registerObservableAttribute } from "../engine/engine_element_extras.js";
5
5
  import { InputEvents } from "../engine/engine_input.js";
6
6
  import { isLocalNetwork } from "../engine/engine_networking_utils.js";
@@ -64,6 +64,7 @@
64
64
  * - `loadscene-start`: Called when a scene starts loading
65
65
  * - `loadscene-finished`: Called when a scene finished loading
66
66
  * - `progress`: Called when a scene is loading and the progress changes
67
+ * - `scene-opened`: Called when a scene is loaded and added to the SceneSwitcher's GameObject
67
68
  * @example
68
69
  * ```ts
69
70
  * sceneSwitcher.addEventListener("loadscene-start", (e) => {
@@ -75,6 +76,9 @@
75
76
  * sceneSwitcher.addEventListener("progress", (e) => {
76
77
  * console.log("Loading progress", e.loaded, e.total);
77
78
  * });
79
+ * sceneSwitcher.addEventListener("scene-opened", (e) => {
80
+ * console.log("Scene opened", e.detail.scene.uri);
81
+ * });
78
82
  * ```
79
83
  *
80
84
  */
@@ -159,10 +163,14 @@
159
163
 
160
164
  private _preloadScheduler?: PreLoadScheduler;
161
165
 
166
+ /** @internal */
162
167
  awake(): void {
168
+ if (this.scenes === undefined) this.scenes = [];
169
+
163
170
  if (debug) console.log("SceneSwitcher", this);
164
171
  }
165
172
 
173
+ /** @internal */
166
174
  async onEnable() {
167
175
  globalThis.addEventListener("popstate", this.onPopState);
168
176
  this.context.input.addEventListener(InputEvents.KeyDown, this.onInputKeyDown);
@@ -210,6 +218,7 @@
210
218
  }
211
219
  }
212
220
 
221
+ /** @internal */
213
222
  onDisable(): void {
214
223
  globalThis.removeEventListener("popstate", this.onPopState);
215
224
  this.context.input.removeEventListener(InputEvents.KeyDown, this.onInputKeyDown);
@@ -288,14 +297,58 @@
288
297
  }
289
298
  }
290
299
 
300
+ /**
301
+ * Add a scene to the SceneSwitcher.
302
+ * If the scene is already added it will be added again.
303
+ * @param urlOrAssetReference The url of the scene or an AssetReference to the scene
304
+ * @returns The AssetReference of the scene that was added
305
+ * @example
306
+ * ```ts
307
+ * // adding a scene:
308
+ * sceneSwitcher.addScene("scene1.glb");
309
+ * // add another scene and load it:
310
+ * const scene2 = sceneSwitcher.addScene("scene2.glb");
311
+ * sceneSwitcher.switchScene(scene2).then(res => { console.log("Scene loaded", res); });
312
+ * ```
313
+ */
314
+ addScene(urlOrAssetReference: string | AssetReference): AssetReference {
315
+ if (typeof urlOrAssetReference === "string") {
316
+ let assetReference = this.context.addressables.findAssetReference(urlOrAssetReference);
317
+ if (!assetReference) {
318
+ assetReference = new AssetReference(urlOrAssetReference);
319
+ this.context.addressables.registerAssetReference(assetReference);
320
+ }
321
+ this.scenes.push(assetReference);
322
+ return assetReference;
323
+ }
324
+
325
+ this.scenes.push(urlOrAssetReference);
326
+ return urlOrAssetReference;
327
+ }
328
+
329
+ /**
330
+ * Load the next scene in the scenes array ({@link this.currentIndex} + 1)
331
+ * If the current scene is the last scene in the array and {@link this.clamp} is disabled then the first scene will be loaded.
332
+ * @returns a promise that resolves to true if the scene was loaded successfully
333
+ */
291
334
  selectNext(): Promise<boolean> {
292
335
  return this.select(this._currentIndex + 1);
293
336
  }
294
337
 
338
+ /**
339
+ * Load the previous scene in the scenes array ({@link this.currentIndex} - 1)
340
+ * If the current scene is the first scene in the array and {@link this.clamp} is disabled then the last scene will be loaded.
341
+ * @returns a promise that resolves to true if the scene was loaded successfully
342
+ */
295
343
  selectPrev(): Promise<boolean> {
296
344
  return this.select(this._currentIndex - 1);
297
345
  }
298
346
 
347
+ /**
348
+ * Load a scene by its index in the scenes array.
349
+ * @param index The index of the scene or a string that represents the scene uri (if the url is not known to the SceneSwitcher it will try to load the scene by its uri but it won't be added to the current scenes array. Use {@link addScene} to add a scene to the SceneSwitcher)
350
+ * @returns a promise that resolves to true if the scene was loaded successfully
351
+ */
299
352
  select(index: number | string): Promise<boolean> {
300
353
  if (debug) console.log("select", index);
301
354
 
@@ -341,6 +394,19 @@
341
394
  private __lastSwitchScene?: AssetReference;
342
395
  private __lastSwitchScenePromise?: Promise<boolean>;
343
396
 
397
+ /**
398
+ * Switch to a scene by its AssetReference.
399
+ * If the scene is already loaded it will be unloaded and the new scene will be loaded.
400
+ * If the scene is already loading it will wait for the scene to be loaded.
401
+ * If the scene is already loaded and the same scene is requested again it will return the same promise that was returned the first time the scene was requested.
402
+ * @param scene The AssetReference of the scene to switch to
403
+ * @returns a promise that resolves to true if the scene was loaded successfully
404
+ * @example
405
+ * ```ts
406
+ * const myAssetReference = new AssetReference("scene1.glb");
407
+ * sceneSwitcher.switchScene(myAssetReference).then(res => { console.log("Scene loaded", res); });
408
+ * ```
409
+ */
344
410
  async switchScene(scene: AssetReference): Promise<boolean> {
345
411
  if (!(scene instanceof AssetReference)) {
346
412
  const type = typeof scene;
@@ -444,6 +510,9 @@
444
510
  const res = sceneListener.sceneOpened(this);
445
511
  if (res instanceof Promise) await res;
446
512
  }
513
+
514
+ const openedEvt = new CustomEvent<LoadSceneEvent>("scene-opened", { detail: { scene: scene, switcher: this, index: index } });
515
+ this.dispatchEvent(openedEvt);
447
516
  return true;
448
517
  }
449
518
  }