Needle Engine

Changes between version 3.1.0-alpha.2 and 3.2.0-alpha
Files changed (25) hide show
  1. src/engine/codegen/register_types.js +2 -0
  2. src/engine-components/AudioSource.ts +6 -3
  3. src/engine-components/Camera.ts +1 -1
  4. src/engine-components/Component.ts +0 -1
  5. src/engine-components/codegen/components.ts +1 -0
  6. src/engine/debug/debug_overlay.ts +1 -0
  7. src/engine/engine_context_registry.ts +3 -0
  8. src/engine/engine_gltf_builtin_components.ts +3 -0
  9. src/engine/engine_license.ts +1 -1
  10. src/engine/engine_lightdata.ts +1 -1
  11. src/engine/engine_networking_auto.ts +13 -6
  12. src/engine/engine_rendererdata.ts +35 -16
  13. src/engine/engine_serialization_builtin_serializer.ts +10 -1
  14. src/engine/extensions/extensions.ts +6 -0
  15. src/engine/extensions/NEEDLE_lighting_settings.ts +15 -13
  16. src/engine/extensions/NEEDLE_lightmaps.ts +3 -2
  17. src/engine/extensions/NEEDLE_progressive.ts +9 -3
  18. src/engine-components/timeline/PlayableDirector.ts +0 -1
  19. src/engine-components/Renderer.ts +69 -20
  20. src/engine-components/RendererLightmap.ts +7 -11
  21. src/engine-components/ScreenCapture.ts +1 -0
  22. src/engine-components/timeline/TimelineTracks.ts +9 -3
  23. src/engine-components/VideoPlayer.ts +16 -11
  24. src/engine-components/XRFlag.ts +3 -0
  25. src/engine-components/SceneSwitcher.ts +224 -0
src/engine/codegen/register_types.js CHANGED
@@ -119,6 +119,7 @@
119
119
  import { Rigidbody } from "../../engine-components/RigidBody";
120
120
  import { RotationBySpeedModule } from "../../engine-components/ParticleSystemModules";
121
121
  import { RotationOverLifetimeModule } from "../../engine-components/ParticleSystemModules";
122
+ import { SceneSwitcher } from "../../engine-components/SceneSwitcher";
122
123
  import { ScreenCapture } from "../../engine-components/ScreenCapture";
123
124
  import { ScreenSpaceAmbientOcclusion } from "../../engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusion";
124
125
  import { ShadowCatcher } from "../../engine-components/ShadowCatcher";
@@ -303,6 +304,7 @@
303
304
  TypeStore.add("Rigidbody", Rigidbody);
304
305
  TypeStore.add("RotationBySpeedModule", RotationBySpeedModule);
305
306
  TypeStore.add("RotationOverLifetimeModule", RotationOverLifetimeModule);
307
+ TypeStore.add("SceneSwitcher", SceneSwitcher);
306
308
  TypeStore.add("ScreenCapture", ScreenCapture);
307
309
  TypeStore.add("ScreenSpaceAmbientOcclusion", ScreenSpaceAmbientOcclusion);
308
310
  TypeStore.add("ShadowCatcher", ShadowCatcher);
src/engine-components/AudioSource.ts CHANGED
@@ -143,6 +143,7 @@
143
143
  @serializable()
144
144
  rollOffMode: AudioRolloffMode = 0;
145
145
 
146
+ playInBackground: boolean = true;
146
147
 
147
148
  private _loop: boolean = false;
148
149
  private sound: THREE.PositionalAudio | null = null;
@@ -196,9 +197,11 @@
196
197
  private onVisibilityChanged = () => {
197
198
  switch (document.visibilityState) {
198
199
  case "hidden":
199
- this.wasPlaying = this.isPlaying;
200
- if (this.isPlaying) {
201
- this.pause();
200
+ if (this.playInBackground === false) {
201
+ this.wasPlaying = this.isPlaying;
202
+ if (this.isPlaying) {
203
+ this.pause();
204
+ }
202
205
  }
203
206
  break;
204
207
  case "visible":
src/engine-components/Camera.ts CHANGED
@@ -390,7 +390,7 @@
390
390
  enable() {
391
391
  this._skybox = this.context.lightmaps.tryGetSkybox(this._camera.sourceId) as THREE.Texture;
392
392
  if (!this._skybox) {
393
- console.warn("Failed to load/find skybox texture", this);
393
+ console.warn("Failed to load/find skybox texture", this._camera.sourceId, this.context.lightmaps);
394
394
  }
395
395
  else if (this.context.scene.background !== this._skybox) {
396
396
  if (debug)
src/engine-components/Component.ts CHANGED
@@ -9,7 +9,6 @@
9
9
  import { ConstructorConcrete, SourceIdentifier, IComponent, IGameObject, Constructor, GuidsMap, UIDProvider, Collision, ICollider } from "../engine/engine_types";
10
10
  import { addNewComponent, destroyComponentInstance, findObjectOfType, findObjectsOfType, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, moveComponentInstance, removeComponent } from "../engine/engine_components";
11
11
  import { findByGuid, destroy, InstantiateOptions, instantiate, HideFlags, foreachComponent, markAsInstancedRendered, isActiveInHierarchy, isActiveSelf, isUsingInstancing, setActive, isDestroyed } from "../engine/engine_gameobject";
12
- import { resolveUrl } from "../engine/engine_utils";
13
12
 
14
13
 
15
14
  // export interface ISerializationCallbackReceiver {
src/engine-components/codegen/components.ts CHANGED
@@ -114,6 +114,7 @@
114
114
  export { Rigidbody } from "../RigidBody";
115
115
  export { RotationBySpeedModule } from "../ParticleSystemModules";
116
116
  export { RotationOverLifetimeModule } from "../ParticleSystemModules";
117
+ export { SceneSwitcher } from "../SceneSwitcher";
117
118
  export { ScreenCapture } from "../ScreenCapture";
118
119
  export { ScreenSpaceAmbientOcclusion } from "../postprocessing/Effects/ScreenspaceAmbientOcclusion";
119
120
  export { ShadowCatcher } from "../ShadowCatcher";
src/engine/debug/debug_overlay.ts CHANGED
@@ -24,6 +24,7 @@
24
24
  export function makeErrorsVisibleForDevelopment() {
25
25
  if (hide) return;
26
26
  const isLocal = isLocalNetwork();
27
+ if(debug) console.log("Is this a local network?", isLocal);
27
28
  if (isLocal) {
28
29
  if (debug)
29
30
  console.log(window.location.hostname);
src/engine/engine_context_registry.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { IContext } from "./engine_types";
2
2
 
3
3
  export enum ContextEvent {
4
+ /** called when the context is registered to the registry, the context is not fully initialized at this point */
5
+ ContextRegistered = "ContextRegistered",
4
6
  ContextCreated = "ContextCreated",
5
7
  ContextDestroyed = "ContextDestroyed",
6
8
  MissingCamera = "MissingCamera",
@@ -26,6 +28,7 @@
26
28
 
27
29
  static register(ctx: IContext) {
28
30
  this.Registered.push(ctx);
31
+ this.dispatchCallback(ContextEvent.ContextRegistered, ctx);
29
32
  }
30
33
 
31
34
  static unregister(ctx: IContext) {
src/engine/engine_gltf_builtin_components.ts CHANGED
@@ -50,6 +50,9 @@
50
50
  idProvider = new InstantiateIdProvider(seed as number);
51
51
  }
52
52
 
53
+ const idEnd = gltfId.indexOf("?");
54
+ gltfId = idEnd === -1 ? gltfId : gltfId.substring(0, idEnd);
55
+
53
56
  const serializationContext = new SerializationContext(gltf.scene);
54
57
  serializationContext.gltfId = gltfId;
55
58
  serializationContext.context = context;
src/engine/engine_license.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  }
15
15
 
16
16
 
17
- ContextRegistry.registerCallback(ContextEvent.ContextCreated, evt => {
17
+ ContextRegistry.registerCallback(ContextEvent.ContextRegistered, evt => {
18
18
  showLicenseInfo(evt.context);
19
19
  });
20
20
 
src/engine/engine_lightdata.ts CHANGED
@@ -33,7 +33,7 @@
33
33
  }
34
34
 
35
35
  registerTexture(sourceId: SourceIdentifier, type: LightmapType, tex: Texture, index: number) {
36
- if (debugLightmap) console.log("Registering ", LightmapType[type], tex, sourceId);
36
+ if (debugLightmap) console.log("Registering ", LightmapType[type] + " \"" + sourceId + "\"", tex);
37
37
  if (!this._lightmaps.has(sourceId))
38
38
  this._lightmaps.set(sourceId, new Map());
39
39
  const map = this._lightmaps.get(sourceId);
src/engine/engine_networking_auto.ts CHANGED
@@ -200,22 +200,29 @@
200
200
  onPropertyChanged: Function,
201
201
  };
202
202
 
203
+ export declare type FieldChangedCallbackFn = (newValue: any, previousValue: any) => void | boolean;
204
+
203
205
  /**
204
206
  * Decorate a field to be automatically networked synced
205
- * @param onFieldChanged name of a callback function that will be called when the field is changed.
206
- * This function may return false to prevent notifyChanged from being called
207
+ * @param onFieldChanged name of a callback function that will be called when the field is changed.
208
+ * You can also pass in a function like so: syncField(myClass.prototype.myFunctionToBeCalled)
209
+ * This function may return false to prevent notifyChanged from being called
207
210
  * (for example a networked color is sent as a number and may be converted to a color in the receiver again)
208
- *
209
211
  * Parameters: (newValue, previousValue)
210
- * @returns
211
212
  */
212
- export const syncField = function (onFieldChanged?: string) {
213
+ export const syncField = function(onFieldChanged: string | FieldChangedCallbackFn) {
213
214
 
214
215
  return function (target: any, propertyKey: string) {
215
216
 
216
217
  let syncer: ComponentPropertiesSyncer | null = null;
217
- const fn = onFieldChanged ? target[onFieldChanged] : undefined;
218
218
 
219
+ let fn: Function | undefined = undefined;
220
+ if (typeof onFieldChanged === "string")
221
+ fn = target[onFieldChanged];
222
+ else if (typeof onFieldChanged === "function") {
223
+ fn = onFieldChanged;
224
+ }
225
+
219
226
  const t = target;
220
227
  const internalAwake = t.__internalAwake;
221
228
  if (debug)
src/engine/engine_rendererdata.ts CHANGED
@@ -36,7 +36,8 @@
36
36
  this.context.pre_update_callbacks.push(this.preUpdate.bind(this))
37
37
  }
38
38
 
39
- private sceneLightSettings?: SceneLightSettings;
39
+ private _currentReflectionId?: SourceIdentifier;
40
+ private sceneLightSettings?: Map<SourceIdentifier, SceneLightSettings>;
40
41
 
41
42
  private preUpdate() {
42
43
  const time = this.context.time;
@@ -53,14 +54,26 @@
53
54
 
54
55
  get environmentIntensity(): number {
55
56
  if (!this.sceneLightSettings) return 1;
56
- return this.sceneLightSettings.ambientIntensity;// * Math.PI * .5;
57
+ if (!this._currentReflectionId) return 1;
58
+ const settings = this.sceneLightSettings.get(this._currentReflectionId);
59
+ if(settings)
60
+ return settings.ambientIntensity;// * Math.PI * .5;
61
+ return 1;
57
62
  }
58
63
 
59
64
  registerSceneLightSettings(sceneLightSettings: SceneLightSettings) {
60
- this.sceneLightSettings = sceneLightSettings;
65
+ const sourceId = sceneLightSettings.sourceId;
66
+ if(!sourceId){
67
+ console.error("Missing source id for scene light settings, can not register:", sceneLightSettings);
68
+ return;
69
+ }
70
+ if (debug) console.log("Register lighting settings", sceneLightSettings?.sourceId, sceneLightSettings);
71
+ if (!this.sceneLightSettings) this.sceneLightSettings = new Map();
72
+ this.sceneLightSettings.set(sourceId, sceneLightSettings);
61
73
  }
62
74
 
63
75
  registerReflection(sourceId: SourceIdentifier, reflectionTexture: Texture) {
76
+ if (debug) console.log("Register reflection", sourceId, reflectionTexture);
64
77
  const h = new LightData(this.context, reflectionTexture, 1);
65
78
  this._lighting[sourceId] = h;
66
79
  }
@@ -70,16 +83,21 @@
70
83
  }
71
84
 
72
85
  enableReflection(sourceId: SourceIdentifier) {
86
+ const previousId = this._currentReflectionId;
87
+ this._currentReflectionId = sourceId;
88
+ const settings = this.sceneLightSettings?.get(sourceId);
89
+
73
90
  if (debug) {
74
- console.log(this.sceneLightSettings ? AmbientMode[this.sceneLightSettings.ambientMode] : "Unknown ambient mode");
91
+ console.log("Enable reflection", sourceId, settings ? AmbientMode[settings.ambientMode] : "Unknown ambient mode");
75
92
  }
76
93
 
77
- switch (this.sceneLightSettings?.ambientMode) {
94
+ switch (settings?.ambientMode) {
78
95
  case AmbientMode.Skybox:
79
96
  case AmbientMode.Custom:
80
97
  // only set environment reflection when ambient mode is skybox or custom
81
98
  const existing = this.getReflection(sourceId);
82
99
  if (existing && existing.Source) {
100
+ if (debug) console.log("Setting environment reflection", existing.Source);
83
101
  const scene = this.context.scene;
84
102
  const tex = existing.Source;
85
103
  tex.encoding = sRGBEncoding;
@@ -87,29 +105,30 @@
87
105
  scene.environment = tex;
88
106
  return;
89
107
  }
108
+ else if (debug) console.warn("Could not find reflection for source", sourceId);
90
109
  break;
91
110
  }
92
111
 
93
- if (this.sceneLightSettings?.environmentReflectionSource === DefaultReflectionMode.Custom) {
94
- switch (this.sceneLightSettings?.ambientMode) {
112
+ if (settings?.environmentReflectionSource === DefaultReflectionMode.Custom) {
113
+ switch (settings?.ambientMode) {
95
114
  case AmbientMode.Trilight:
96
- if (this.sceneLightSettings.ambientTrilight) {
97
- const colors = this.sceneLightSettings.ambientTrilight;
115
+ if (settings.ambientTrilight) {
116
+ const colors = settings.ambientTrilight;
98
117
  const tex = createTrilightTexture(colors[0], colors[1], colors[2], 64, 64);
99
118
  tex.encoding = sRGBEncoding;
100
119
  tex.mapping = EquirectangularReflectionMapping;
101
120
  this.context.scene.environment = tex;
102
121
  }
103
- else console.error("Missing ambient trilight", this.sceneLightSettings.sourceId);
122
+ else console.error("Missing ambient trilight", settings.sourceId);
104
123
  return;
105
124
  case AmbientMode.Flat:
106
- if (this.sceneLightSettings.ambientLight) {
107
- const tex = createFlatTexture(this.sceneLightSettings.ambientLight, 64);
125
+ if (settings.ambientLight) {
126
+ const tex = createFlatTexture(settings.ambientLight, 64);
108
127
  tex.encoding = sRGBEncoding;
109
128
  tex.mapping = EquirectangularReflectionMapping;
110
129
  this.context.scene.environment = tex;
111
130
  }
112
- else console.error("Missing ambientlight", this.sceneLightSettings.sourceId);
131
+ else console.error("Missing ambientlight", settings.sourceId);
113
132
  return;
114
133
  default:
115
134
  return;
@@ -124,7 +143,7 @@
124
143
 
125
144
  async getSceneLightingData(sourceId: SourceIdentifier): Promise<SphericalHarmonicsData> {
126
145
  if (debug)
127
- console.log("GET SCENE LIGHT DATA");
146
+ console.log("GET SCENE LIGHT DATA", sourceId);
128
147
 
129
148
  // const existing = this.getReflection(sourceId);
130
149
  // const sh = existing?.getSphericalHarmonicsArray(this.sceneLightSettings?.ambientIntensity ?? 1);
@@ -140,7 +159,7 @@
140
159
  const ex = this.getReflection(sourceId);
141
160
  if (ex) {
142
161
  clearInterval(interval);
143
- res(ex.getSphericalHarmonicsArray(this.sceneLightSettings?.ambientIntensity ?? 1)!);
162
+ res(ex.getSphericalHarmonicsArray(this.environmentIntensity ?? 1)!);
144
163
  }
145
164
  }, 10);
146
165
  });
@@ -181,7 +200,7 @@
181
200
  const reflection = this._source;
182
201
  let rt: THREE.WebGLCubeRenderTarget | null = null;
183
202
  if (reflection) {
184
- if (debug) console.log("GENERATING LIGHT PROBE", reflection);
203
+ if (debug) console.log("GENERATING LIGHT PROBE", reflection, this.Source);
185
204
  const size = Math.min(reflection.image.width, 512);
186
205
  const target = new WebGLCubeRenderTarget(size);
187
206
  rt = target.fromEquirectangularTexture(this._context.renderer, reflection);
src/engine/engine_serialization_builtin_serializer.ts CHANGED
@@ -240,7 +240,16 @@
240
240
  }
241
241
  const hasMethod = call.method?.length > 0;
242
242
  if (target && hasMethod) {
243
- const printWarningMethodNotFound = () => console.warn(`Could not find method ${call.method} on object ${target.name}`, target, typeof target[call.method]);
243
+ const printWarningMethodNotFound = () => {
244
+ const uppercaseMethodName = call.method[0].toLowerCase() + call.method.slice(1);
245
+ if (typeof target[uppercaseMethodName] === "function") {
246
+ console.warn(`Could not find method ${call.method} on object ${target.name}. Please rename ${call.method} to ${uppercaseMethodName}?`, target, typeof target[call.method]);
247
+ return;
248
+ }
249
+ else {
250
+ console.warn(`Could not find method ${call.method} on object ${target.name}`, target, typeof target[call.method]);
251
+ }
252
+ }
244
253
  const method = target[call.method];
245
254
  if (typeof method !== "function") {
246
255
  let foundMethod = false;
src/engine/extensions/extensions.ts CHANGED
@@ -51,6 +51,12 @@
51
51
  }
52
52
 
53
53
  export function registerExtensions(loader: GLTFLoader, context: Context, sourceId: SourceIdentifier) {
54
+
55
+ // Make sure to remove any url parameters from the sourceId (because the source id in the renderer does not have a ?v=xxx so it will not be able to register the resolved lightmap otherwise)
56
+ const idEnd = sourceId.lastIndexOf("?");
57
+ if (idEnd >= 0) sourceId = sourceId.substring(0, idEnd);
58
+
59
+
54
60
  loader.register(p => new NEEDLE_gameobject_data(p));
55
61
  loader.register(p => new NEEDLE_persistent_assets(p));
56
62
  loader.register(p => new NEEDLE_lightmaps(p, context.lightmaps, sourceId));
src/engine/extensions/NEEDLE_lighting_settings.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AmbientLight, Color, HemisphereLight } from "three";
1
+ import { AmbientLight, Color, HemisphereLight, Object3D } from "three";
2
2
  import { GLTF, GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader";
3
3
  import { SourceIdentifier } from "../engine_types";
4
4
  import { Behaviour, GameObject } from "../../engine-components/Component";
@@ -42,9 +42,12 @@
42
42
  const ext: LightingSettings = extensions[EXTENSION_NAME];
43
43
  if (ext) {
44
44
  if (debug)
45
- console.log(ext);
45
+ console.log("Apply \"" + this.name + "\", src: \"" + this.sourceId + "\"", ext);
46
46
  // add a component to the root of the scene
47
- const settings = GameObject.addNewComponent(_result.scene, SceneLightSettings, false);
47
+ const lightSettings = new Object3D();
48
+ lightSettings.name = "Needle LightSettings";
49
+ _result.scene.add(lightSettings);
50
+ const settings = GameObject.addNewComponent(lightSettings, SceneLightSettings, false);
48
51
  settings.sourceId = this.sourceId;
49
52
  settings.ambientIntensity = ext.ambientIntensity;
50
53
  settings.ambientLight = new Color().fromArray(ext.ambientLight);
@@ -98,12 +101,11 @@
98
101
 
99
102
  onEnable() {
100
103
  const isActive = this.context.mainCameraComponent?.sourceId === this.sourceId;
101
- if (debug)
102
- console.log("Enable scene lighting", this.sourceId, isActive, this, this.context.mainCameraComponent?.sourceId);
104
+ if (debug) console.log("Enable scene lighting", this.sourceId, isActive, this, this.context.mainCameraComponent?.sourceId);
103
105
  if (!isActive) {
104
- if(debug) console.warn("This is no active?!", this.context.mainCameraComponent?.sourceId)
105
- // this.enabled = false;
106
- // return;
106
+ if(debug) console.warn("This environment light is not active??!", this.context.mainCameraComponent?.sourceId)
107
+ this.enabled = false;
108
+ return;
107
109
  }
108
110
  if (this.ambientMode == AmbientMode.Flat) {
109
111
  if (this.ambientLight && !this._ambientLightObj) {
@@ -130,19 +132,19 @@
130
132
  if (!this._lightProbeObj) {
131
133
  if (this.sourceId) {
132
134
  this.context.rendererData.getSceneLightingData(this.sourceId).then(data => {
133
- if (debug)
134
- console.log(data);
135
135
  if (!data) return;
136
136
  this._lightProbeObj = data.lightProbe;
137
137
  if (this.enabled && !this.destroyed && this._lightProbeObj) {
138
- this.scene.add(this._lightProbeObj);
138
+ if (debug)
139
+ console.log("Add", this.sourceId, data);
140
+ this.gameObject.add(this._lightProbeObj);
139
141
  }
140
142
  });
141
143
  }
142
144
  }
143
145
  else {
144
146
  if (this.enabled && this.destroyed && this._lightProbeObj) {
145
- this.scene.add(this._lightProbeObj);
147
+ this.gameObject.add(this._lightProbeObj);
146
148
  }
147
149
  }
148
150
  }
@@ -154,7 +156,7 @@
154
156
 
155
157
  onDisable() {
156
158
  if (debug)
157
- console.log("disable", this.sourceId, this);
159
+ console.log("Disable envlight:", this.sourceId, this);
158
160
  if (this._lightProbeObj) this._lightProbeObj.removeFromParent();
159
161
  if(this._ambientLightObj) this._ambientLightObj.removeFromParent();
160
162
  if (this.sourceId)
src/engine/extensions/NEEDLE_lightmaps.ts CHANGED
@@ -58,6 +58,7 @@
58
58
  console.log(ext);
59
59
 
60
60
  return new Promise(async (res, _rej) => {
61
+
61
62
  const dependencies: Array<Promise<any>> = [];
62
63
  for (const entry of arr) {
63
64
  if (entry.pointer) {
@@ -70,10 +71,10 @@
70
71
  // TODO this is most likely wrong for floating point textures
71
72
  if (entry.type !== LightmapType.Lightmap)
72
73
  tex.encoding = sRGBEncoding;
73
- else
74
+ else
74
75
  tex.encoding = LinearEncoding;
75
76
 
76
-
77
+
77
78
  // Dont flip skybox textures anymore - previously we exported them flipped when baking in Unity but now we allow to pass through export without re-baking exisitng skybox textures if they use default values. So we expect textures to be NOT flipped anymore
78
79
  // if (entry.type === LightmapType.Skybox) {
79
80
  // if (tex.type == FloatType || tex.type == HalfFloatType)
src/engine/extensions/NEEDLE_progressive.ts CHANGED
@@ -138,10 +138,16 @@
138
138
  }
139
139
  const resolveKey = uri + "_" + progressiveInfo.guid;
140
140
  if (this.resolved[resolveKey]) {
141
- if (debug) console.log("Texture has already been loaded: " + resolveKey, material.name, slot, current.name);
142
141
  let res = this.resolved[resolveKey];
143
- res = this.copySettings(current, res);
144
- return res;
142
+ // check if the texture has been disposed or not
143
+ if (res.image && res.image.data) {
144
+ if (debug) console.log("Texture has already been loaded: " + resolveKey, material.name, slot, current.name, res);
145
+ res = this.copySettings(current, res);
146
+ return res;
147
+ }
148
+ else if (res) {
149
+ if(debug) console.log("Texture has been disposed, will load again: " + resolveKey, material.name, slot, current.name, res);
150
+ }
145
151
  }
146
152
 
147
153
  const info = this.onProgressiveLoadStart(context, source, uri, material, slot);
src/engine-components/timeline/PlayableDirector.ts CHANGED
@@ -452,7 +452,6 @@
452
452
  const anim = GameObject.getOrAddComponent(binding, Animator);
453
453
  if(anim) binding = anim;
454
454
  }
455
- if (typeof binding.enabled === "boolean") binding.enabled = false;
456
455
  const animationClips = binding?.gameObject?.animations;
457
456
  if (animationClips) {
458
457
  const handler = new Tracks.AnimationTrackHandler();
src/engine-components/Renderer.ts CHANGED
@@ -59,6 +59,18 @@
59
59
  private _indexMapMaxIndex?: number;
60
60
  private _indexMap?: Map<number, number>;
61
61
 
62
+ private _changed: boolean = false;
63
+ get changed(): boolean {
64
+ return this._changed;
65
+ }
66
+ set changed(value: boolean) {
67
+ if (value === true) {
68
+ if (debugRenderer)
69
+ console.warn("SharedMaterials have changed: " + this._renderer.name, this);
70
+ }
71
+ this._changed = value;
72
+ }
73
+
62
74
  is(renderer: Renderer) {
63
75
  return this._renderer === renderer;
64
76
  }
@@ -131,7 +143,12 @@
131
143
  if (typeof key === "string")
132
144
  setMaterial(value, Number.parseInt(key));
133
145
  // console.log(target, key, value);
134
- return Reflect.set(target, key, value);
146
+ if (Reflect.set(target, key, value)) {
147
+ if (value instanceof Material)
148
+ target.changed = true;
149
+ return true;
150
+ }
151
+ return false;
135
152
  }
136
153
  });
137
154
  }
@@ -141,6 +158,13 @@
141
158
  return this._targets.length;
142
159
  }
143
160
 
161
+ // iterator to support: for(const mat of sharedMaterials)
162
+ *[Symbol.iterator]() {
163
+ for (let i = 0; i < this.length; i++) {
164
+ yield this.getMaterial(i);
165
+ }
166
+ }
167
+
144
168
  private resolveIndex(index: number): number {
145
169
  const map = this._indexMap;
146
170
  // if we have a index map it means that some materials were missing
@@ -157,6 +181,7 @@
157
181
  const target = this._targets[index];
158
182
  if (!target || target["material"] === undefined) return;
159
183
  target["material"] = mat;
184
+ this.changed = true;
160
185
  }
161
186
 
162
187
  private getMaterial(index: number) {
@@ -231,7 +256,10 @@
231
256
  }
232
257
 
233
258
  set sharedMaterial(mat: THREE.Material) {
259
+ const cur = this.sharedMaterials[0];
260
+ if (cur === mat) return;
234
261
  this.sharedMaterials[0] = mat;
262
+ this.applyLightmapping();
235
263
  }
236
264
 
237
265
  /**@deprecated please use sharedMaterial */
@@ -241,7 +269,7 @@
241
269
 
242
270
  /**@deprecated please use sharedMaterial */
243
271
  set material(mat: THREE.Material) {
244
- this.sharedMaterials[0] = mat;
272
+ this.sharedMaterial = mat;
245
273
  }
246
274
 
247
275
  private _sharedMaterials!: SharedMaterialArray;
@@ -323,7 +351,6 @@
323
351
 
324
352
  this._reflectionProbe = null;
325
353
 
326
- const type = this.gameObject.type;
327
354
  if (this.isMultiMaterialObject(this.gameObject)) {
328
355
  for (const child of this.gameObject.children) {
329
356
  this.context.addBeforeRenderListener(child, this.onBeforeRenderThree.bind(this));
@@ -359,20 +386,39 @@
359
386
  this.context.addBeforeRenderListener(this.gameObject, this.onBeforeRenderThree.bind(this));
360
387
  }
361
388
 
389
+ this.applyLightmapping();
390
+
391
+ if (showWireframe) {
392
+ for (let i = 0; i < this.sharedMaterials.length; i++) {
393
+ const mat: any = this.sharedMaterials[i];
394
+ if (mat) {
395
+ mat.wireframe = true;
396
+ }
397
+ }
398
+ }
399
+
400
+ }
401
+
402
+ private applyLightmapping() {
362
403
  if (this.lightmapIndex >= 0) {
404
+ const type = this.gameObject.type;
405
+
363
406
  // use the override lightmap if its not undefined
364
407
  const tex = this._lightmapTextureOverride !== undefined
365
408
  ? this._lightmapTextureOverride
366
409
  : this.context.lightmaps.tryGetLightmap(this.sourceId, this.lightmapIndex);
367
410
  if (tex) {
368
- // tex.encoding = THREE.LinearEncoding;
369
- this._lightmaps = [];
411
+ if (!this._lightmaps)
412
+ this._lightmaps = [];
370
413
 
371
414
  if (type === "Mesh") {
372
415
  const mat = this.gameObject["material"];
373
416
  if (!mat?.isMeshBasicMaterial) {
374
- const rm = new RendererLightmap(this.gameObject, this.context);// GameObject.addNewComponent(this.gameObject, RendererLightmap);
375
- this._lightmaps.push(rm);
417
+ if (this._lightmaps.length <= 0) {
418
+ const rm = new RendererLightmap(this.gameObject, this.context);
419
+ this._lightmaps.push(rm);
420
+ }
421
+ const rm = this._lightmaps[0];
376
422
  rm.init(this.lightmapIndex, this.lightmapScaleOffset, tex, debugLightmap);
377
423
  }
378
424
  else {
@@ -383,10 +429,16 @@
383
429
  // for multi materials we need to loop through children
384
430
  // and then we add a lightmap renderer component to each of them
385
431
  else if (this.isMultiMaterialObject(this.gameObject) && this.sharedMaterials.length > 0) {
386
- for (const child of this.gameObject.children) {
432
+ for (let i = 0; i < this.gameObject.children.length; i++) {
433
+ const child = this.gameObject.children[i];
387
434
  if (!child["material"]?.isMeshBasicMaterial) {
388
- const rm = new RendererLightmap(child as GameObject, this.context);
389
- this._lightmaps.push(rm);
435
+ let rm: RendererLightmap | undefined = undefined;
436
+ if (i >= this._lightmaps.length) {
437
+ rm = new RendererLightmap(child as GameObject, this.context);
438
+ this._lightmaps.push(rm);
439
+ }
440
+ else
441
+ rm = this._lightmaps[i];
390
442
  rm.init(this.lightmapIndex, this.lightmapScaleOffset, tex, debugLightmap);
391
443
  // onBeforeRender is not called when the renderer is on a group
392
444
  // this is an issue we probably also need to handle for custom shaders
@@ -396,16 +448,8 @@
396
448
  }
397
449
  }
398
450
  }
399
- }
400
-
401
-
402
-
403
- if (showWireframe) {
404
- for (let i = 0; i < this.sharedMaterials.length; i++) {
405
- const mat: any = this.sharedMaterials[i];
406
- if (mat) {
407
- mat.wireframe = true;
408
- }
451
+ else {
452
+ if (debug) console.warn("Lightmap not found", this.sourceId, this.lightmapIndex);
409
453
  }
410
454
  }
411
455
 
@@ -518,6 +562,11 @@
518
562
  else {
519
563
  this.applySettings(this.gameObject);
520
564
  }
565
+
566
+ if (this.sharedMaterials.changed) {
567
+ this.sharedMaterials.changed = false;
568
+ this.applyLightmapping();
569
+ }
521
570
 
522
571
  if (this.handles?.length) {
523
572
  // if (this.name === "Darbouka")
src/engine-components/RendererLightmap.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  set lightmap(tex: Texture | null) {
15
15
  if (tex !== this.lightmapTexture) {
16
16
  this.lightmapTexture = tex;
17
- this.setupLightmap();
17
+ this.applyLightmap();
18
18
  }
19
19
  }
20
20
 
@@ -27,8 +27,6 @@
27
27
  private lightmapScaleOffsetUniform = { value: new THREE.Vector4(1, 1, 0, 0) };
28
28
  private lightmapUniform: { value: THREE.Texture | null } = { value: null };
29
29
 
30
- private beforeRenderCallback?: OnBeforeRenderCallback;
31
-
32
30
  constructor(gameObject: GameObject, context: Context) {
33
31
  this.gameObject = gameObject;
34
32
  this.context = context;
@@ -44,20 +42,19 @@
44
42
 
45
43
  const debugLightmaps = debug;
46
44
  if (debugLightmaps) this.setLightmapDebugMaterial();
47
- this.setupLightmap();
45
+ this.applyLightmap();
48
46
  }
49
47
 
50
48
  bindOnBeforeRender() {
51
- this.beforeRenderCallback = this.onBeforeRenderThreeComplete.bind(this);
52
- this.context.addBeforeRenderListener(this.gameObject, this.beforeRenderCallback);
53
- // this.gameObject.onBeforeRender = this.onBeforeRenderThreeComplete.bind(this);
49
+ this.context.removeBeforeRenderListener(this.gameObject, this.onBeforeRenderThreeComplete);
50
+ this.context.addBeforeRenderListener(this.gameObject, this.onBeforeRenderThreeComplete);
54
51
  }
55
52
 
56
- private onBeforeRenderThreeComplete(_renderer, _scene, _camera, _geometry, material, _group) {
53
+ private onBeforeRenderThreeComplete = (_renderer, _scene, _camera, _geometry, material, _group) => {
57
54
  this.onBeforeRenderThree(material);
58
55
  }
59
56
 
60
- private setupLightmap() {
57
+ private applyLightmap() {
61
58
 
62
59
  if (this.gameObject.type === "Object3D") {
63
60
  // console.warn("Can not add lightmap. Is this object missing a renderer?");
@@ -65,13 +62,12 @@
65
62
  }
66
63
 
67
64
  if (this.gameObject.type === "Group") {
68
- console.warn("Lightmap on multimaterial object is not supported yet... please ask kindly for implementation.");
65
+ console.warn("Lightmap on multimaterial object is not supported yet... please open a feature request on https://github.com/needle-tools/needle-engine-support if your project requires it");
69
66
  return;
70
67
  }
71
68
 
72
69
  console.assert(this.gameObject.type === "Mesh", "Lightmap only works on meshes", this);
73
70
 
74
-
75
71
  const mesh = this.gameObject as unknown as THREE.Mesh;
76
72
  // TODO: ensure uv2 exists
77
73
  if (!mesh.geometry.getAttribute("uv2"))
src/engine-components/ScreenCapture.ts CHANGED
@@ -96,6 +96,7 @@
96
96
  console.log("Screensharing", this.name, this);
97
97
  AudioSource.registerWaitForAllowAudio(() => {
98
98
  if (this.videoPlayer && this._currentStream && this._currentMode === ScreenCaptureMode.Receiving) {
99
+ this.videoPlayer.playInBackground = true;
99
100
  this.videoPlayer.setVideo(this._currentStream);
100
101
  }
101
102
  });
src/engine-components/timeline/TimelineTracks.ts CHANGED
@@ -145,7 +145,7 @@
145
145
  onPauseChanged() {
146
146
  // When the timeline is paused the original animator will be enabled again if it was before
147
147
  if (this._animator && this._animatorWasEnabled !== undefined) {
148
- this._animator.enabled = this.director.isPaused ? this._animatorWasEnabled : false;
148
+ this._animator.enabled = this.director.isPlaying === false ? this._animatorWasEnabled : false;
149
149
  }
150
150
  }
151
151
 
@@ -225,7 +225,12 @@
225
225
  // We need to disable the animator component in case it also animates
226
226
  // which overrides the timeline
227
227
  this._animator = GameObject.getComponent(this.target, Animator) ?? null;
228
- this._animatorWasEnabled = this._animator?.enabled;
228
+ if (this._animator)
229
+ {
230
+ this._animatorWasEnabled = this._animator.enabled;
231
+ this._animator.enabled = false;
232
+ }
233
+ else this._animatorWasEnabled = false;
229
234
  }
230
235
 
231
236
  // Clip Offsets
@@ -323,6 +328,7 @@
323
328
  // const clip = this.clips[i];
324
329
  let weight = 1;
325
330
  weight *= this.evaluateWeight(time, i, this.models, isActive);
331
+ weight *= this.director.weight;
326
332
 
327
333
  let handleLoop = isInTimeRange;
328
334
  if(doPreExtrapolate){
@@ -370,7 +376,7 @@
370
376
  else action.time = t;
371
377
 
372
378
  action.timeScale = 0;
373
- const effectiveWeight = weight * this.director.weight;
379
+ const effectiveWeight = weight;
374
380
  action.weight = effectiveWeight;
375
381
  action.clampWhenFinished = true;
376
382
  if (!action.isRunning())
src/engine-components/VideoPlayer.ts CHANGED
@@ -52,7 +52,7 @@
52
52
 
53
53
  @serializable()
54
54
  aspectMode: AspectMode = AspectMode.None;
55
-
55
+
56
56
  @serializable(URL)
57
57
  private clip?: string | MediaStream | null = null;
58
58
 
@@ -106,12 +106,12 @@
106
106
  get isPlaying(): boolean {
107
107
  const video = this._videoElement;
108
108
  if (video) {
109
- if(video.currentTime > 0 && !video.paused && !video.ended
109
+ if (video.currentTime > 0 && !video.paused && !video.ended
110
110
  && video.readyState > video.HAVE_CURRENT_DATA)
111
111
  return true;
112
- else if(video.srcObject){
112
+ else if (video.srcObject) {
113
113
  const stream = video.srcObject as MediaStream;
114
- if(stream.active) return true;
114
+ if (stream.active) return true;
115
115
  }
116
116
  }
117
117
  return false;
@@ -149,6 +149,9 @@
149
149
  }
150
150
  private _muted: boolean = false;
151
151
 
152
+ /** Set this to false to pause video playback while the tab is not active */
153
+ playInBackground: boolean = true;
154
+
152
155
  private _crossOrigin: string | null = "anonymous";
153
156
 
154
157
  private audioOutputMode: VideoAudioOutputMode = VideoAudioOutputMode.AudioSource;
@@ -198,11 +201,13 @@
198
201
  window.addEventListener('visibilitychange', _evt => {
199
202
  switch (document.visibilityState) {
200
203
  case "hidden":
201
- this.wasPlaying = this._isPlaying;
202
- this.pause();
204
+ if(!this.playInBackground){
205
+ this.wasPlaying = this._isPlaying;
206
+ this.pause();
207
+ }
203
208
  break;
204
209
  case "visible":
205
- if (this.wasPlaying) this.play();
210
+ if (this.wasPlaying && !this._isPlaying) this.play();
206
211
  break;
207
212
  }
208
213
  });
@@ -260,7 +265,7 @@
260
265
  this.updateVideoElementSettings();
261
266
  this._videoElement?.play().catch(err => {
262
267
  // https://developer.chrome.com/blog/play-request-was-interrupted/
263
- if(debug)
268
+ if (debug)
264
269
  console.error("Error playing video", err, "CODE=" + err.code, this.videoElement?.src, this);
265
270
  setTimeout(() => {
266
271
  if (this._isPlaying && !this.destroyed && this.activeAndEnabled)
@@ -318,7 +323,7 @@
318
323
  this._videoTexture.flipY = false;
319
324
  this._videoTexture.encoding = THREE.sRGBEncoding;
320
325
  this.handleBeginPlaying(playAutomatically);
321
- if(debug)
326
+ if (debug)
322
327
  console.log(this);
323
328
  }
324
329
 
@@ -420,7 +425,7 @@
420
425
  // dont open in fullscreen on ios
421
426
  this._videoElement.playsInline = true;
422
427
  let muted = !this._receivedInput && this.audioOutputMode !== VideoAudioOutputMode.None;
423
- if(!muted && this._muted) muted = true;
428
+ if (!muted && this._muted) muted = true;
424
429
  this._videoElement.muted = muted;
425
430
  if (this.playOnAwake)
426
431
  this._videoElement.autoplay = true;
@@ -540,7 +545,7 @@
540
545
  if (!this._screenspaceModeQuad) return;
541
546
  this._screenspaceModeQuad.geometry.scale(2, 2, 2);
542
547
  }
543
-
548
+
544
549
  const quad = this._screenspaceModeQuad;
545
550
  this.context.scene.add(quad);
546
551
  this.updateScreenspaceMaterialUniforms();
src/engine-components/XRFlag.ts CHANGED
@@ -79,6 +79,9 @@
79
79
  XRFlag.firstApply = true;
80
80
  XRFlag.Apply();
81
81
  }
82
+ else {
83
+ this.UpdateVisible(XRState.Global);
84
+ }
82
85
  }
83
86
 
84
87
  onDestroy(): void {
src/engine-components/SceneSwitcher.ts ADDED
@@ -0,0 +1,224 @@
1
+ import { AssetReference } from "../engine/engine_addressables";
2
+ import { NeedleEngineHTMLElement } from "../engine/engine_element";
3
+ import { InputEvents } from "../engine/engine_input";
4
+ import { isLocalNetwork } from "../engine/engine_networking_utils";
5
+ import { ContextEvent, ContextRegistry } from "../engine/engine_context_registry";
6
+ import { getParam, isMobileDevice, setParamWithoutReload } from "../engine/engine_utils";
7
+ import { serializable } from "../engine/engine_serialization";
8
+ import { Behaviour, GameObject } from "./Component";
9
+
10
+ const ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME = "scene";
11
+
12
+ ContextRegistry.registerCallback(ContextEvent.ContextRegistered, _ => {
13
+ if (!NeedleEngineHTMLElement.observedAttributes.includes(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME))
14
+ NeedleEngineHTMLElement.observedAttributes.push(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME);
15
+ });
16
+
17
+ const couldNotLoadScenePromise = Promise.resolve(false);
18
+
19
+ export class SceneSwitcher extends Behaviour {
20
+
21
+ @serializable(AssetReference)
22
+ scenes!: AssetReference[];
23
+
24
+ /** the url parameter that is set/used to store the currently loaded scene in, set to "" to disable */
25
+ @serializable()
26
+ queryParameterName: string = "scene";
27
+
28
+ /** when enabled the new scene is pushed to the browser navigation history, only works with a valid query parameter set */
29
+ @serializable()
30
+ useHistory: boolean = true;
31
+
32
+ /** when enabled you can switch between scenes using keyboard left, right, A and D or number keys */
33
+ @serializable()
34
+ useKeyboard: boolean = true;
35
+
36
+ /** when enabled you can switch between scenes using swipe (mobile only) */
37
+ @serializable()
38
+ useSwipe: boolean = true;
39
+
40
+ private _currentIndex: number = -1;
41
+ private _currentScene: AssetReference | undefined = undefined;
42
+ private _engineElementOverserver: MutationObserver | undefined = undefined;
43
+
44
+ start() {
45
+ if (!this.tryLoadFromQueryParam()) {
46
+ const value = this.context.domElement.getAttribute(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME);
47
+ // let locked = this.lock;
48
+ try {
49
+ // this.lock = false;
50
+ if (value === null || !this.trySelectSceneFromValue(value))
51
+ this.select(0);
52
+ }
53
+ finally {
54
+ // this.lock = locked;
55
+ }
56
+ }
57
+ }
58
+
59
+ onEnable(): void {
60
+ globalThis.addEventListener("popstate", this.onPopState);
61
+ this.context.input.addEventListener(InputEvents.KeyDown, this.onKeyDown);
62
+ this.context.input.addEventListener(InputEvents.PointerMove, this.onPointerMove);
63
+ this.context.input.addEventListener(InputEvents.PointerUp, this.onPointerUp);
64
+
65
+ if (!this._engineElementOverserver) {
66
+ this._engineElementOverserver = new MutationObserver((mutations) => {
67
+ for (const mut of mutations) {
68
+ if (mut.type === "attributes" && mut.attributeName === ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME) {
69
+ const value = this.context.domElement.getAttribute(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME);
70
+ if (value !== null) {
71
+ this.trySelectSceneFromValue(value);
72
+ }
73
+ }
74
+ }
75
+ });
76
+ }
77
+
78
+ this._engineElementOverserver.observe(this.context.domElement, {
79
+ attributes: true
80
+ });
81
+ }
82
+
83
+ onDisable(): void {
84
+ globalThis.removeEventListener("popstate", this.onPopState);
85
+ this.context.input.removeEventListener(InputEvents.KeyDown, this.onKeyDown);
86
+ this.context.input.removeEventListener(InputEvents.PointerMove, this.onPointerMove);
87
+ this.context.input.removeEventListener(InputEvents.PointerUp, this.onPointerUp);
88
+ }
89
+
90
+ private onPopState = async (_state: PopStateEvent) => {
91
+ if (!this.useHistory) return;
92
+ let wasUsingHistory = this.useHistory;
93
+ try {
94
+ this.useHistory = false;
95
+ await this.tryLoadFromQueryParam();
96
+ }
97
+ finally {
98
+ this.useHistory = wasUsingHistory;
99
+ }
100
+ }
101
+
102
+ private normalizedSwipeThresholdX = 0.1;
103
+ private _didSwipe: boolean = false;
104
+ private onPointerMove = (e: any) => {
105
+ if (!this._didSwipe && e.button === 0 && e.pointerType === "touch" && this.context.input.getPointerPressedCount() === 1) {
106
+ const delta = this.context.input.getPointerPositionDelta(e.button);
107
+ if (delta) {
108
+ const normalizedX = delta.x / this.context.domWidth;
109
+ if (normalizedX >= this.normalizedSwipeThresholdX) {
110
+ this._didSwipe = true;
111
+ this.selectPrev();
112
+ }
113
+ else if (normalizedX <= -this.normalizedSwipeThresholdX) {
114
+ this._didSwipe = true;
115
+ this.selectNext();
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ private onPointerUp = (e: any) => {
122
+ if (e.button === 0) {
123
+ this._didSwipe = false;
124
+ }
125
+ };
126
+
127
+ private onKeyDown = (e: any) => {
128
+ if (!this.useKeyboard) return;
129
+ if (!this.scenes) return;
130
+ const key = e.key.toLowerCase();
131
+ if (!key) return;
132
+ const index = parseInt(key) - 1;
133
+ if (index >= 0) {
134
+ this.trySelectSceneFromValue(index);
135
+ return;
136
+ }
137
+ switch (key) {
138
+ case "arrowright":
139
+ case "d":
140
+ this.selectNext();
141
+ break;
142
+ case "arrowleft":
143
+ case "a":
144
+ this.selectPrev();
145
+ break;
146
+ }
147
+ }
148
+
149
+ selectNext() {
150
+ return this.select(this._currentIndex + 1);
151
+ }
152
+
153
+ selectPrev() {
154
+ return this.select(this._currentIndex - 1);
155
+ }
156
+
157
+ select(index: number) {
158
+ if (!this.scenes?.length) return couldNotLoadScenePromise;
159
+ if (index < 0) index = this.scenes.length - 1;
160
+ if (index >= this.scenes.length) index = 0;
161
+ const scene = this.scenes[index];
162
+ return this.switchScene(scene);
163
+ }
164
+
165
+ async switchScene(scene: AssetReference): Promise<boolean> {
166
+ if (scene === this._currentScene) return true;
167
+ if (this._currentScene)
168
+ GameObject.remove(this._currentScene.asset);
169
+ const index = this._currentIndex = this.scenes?.indexOf(scene) ?? -1;
170
+ this._currentScene = scene;
171
+ try {
172
+ await scene.loadAssetAsync();
173
+ if (!scene.asset) return false;
174
+ if (this._currentIndex === index) {
175
+ GameObject.add(scene.asset, this.gameObject);
176
+ // save the loaded scene as an url parameter
177
+ if (this.queryParameterName?.length)
178
+ setParamWithoutReload(this.queryParameterName, index.toString(), this.useHistory);
179
+ return true;
180
+ }
181
+ }
182
+ catch (err) {
183
+ console.error(err);
184
+ }
185
+ return false;
186
+ }
187
+
188
+ private tryLoadFromQueryParam() {
189
+ if (!this.queryParameterName?.length) return false;
190
+ // try restore the scene from the url
191
+ const value = getParam(this.queryParameterName);
192
+ if (typeof value === "boolean") return false;
193
+ return this.trySelectSceneFromValue(value);
194
+ }
195
+
196
+ /** try to select a scene from a string or index */
197
+ private trySelectSceneFromValue(value: string | number) {
198
+
199
+ if (typeof value === "string") {
200
+ const index = parseInt(value as string);
201
+ if (index >= 0 && index < this.scenes.length) {
202
+ return this.select(index);;
203
+ }
204
+ else {
205
+ // Try to find a scene with a matching name
206
+ for (let i = 0; i < this.scenes.length; i++) {
207
+ const scene = this.scenes[i];
208
+ if (scene.uri.toLowerCase().includes(value)) {
209
+ return this.select(i);;
210
+ }
211
+ }
212
+ }
213
+ }
214
+ else if (typeof value === "number") {
215
+ if (value >= 0 && value < this.scenes.length) {
216
+ return this.select(value);;
217
+ }
218
+ }
219
+
220
+ if (isLocalNetwork()) console.warn("Unknown scene value or index: \"" + value + "\"", this)
221
+
222
+ return couldNotLoadScenePromise;
223
+ }
224
+ }