@@ -13,7 +13,8 @@
|
|
13
13
|
export * from "./webxr/index.js"
|
14
14
|
|
15
15
|
export { ClearFlags } from "./Camera.js"
|
16
|
+
export { type ISceneEventListener } from "./SceneSwitcher.js";
|
16
17
|
|
18
|
+
import "./CameraUtils.js"
|
19
|
+
import "./AnimationUtils.js"
|
17
20
|
|
18
|
-
import "./CameraUtils.js"
|
19
|
-
import "./AnimationUtils.js"
|
@@ -17,15 +17,16 @@
|
|
17
17
|
}
|
18
18
|
|
19
19
|
if (!suppressConsole && (showConsole || isLocalNetwork())) {
|
20
|
-
if (isLocalNetwork())
|
21
|
-
|
20
|
+
if (isLocalNetwork()) {
|
21
|
+
const currentUrl = new URL(window.location.href);
|
22
|
+
currentUrl.searchParams.set("console", "1");
|
23
|
+
console.log("🌵 Tip: You can add the \"?console\" query parameter to the url to show the debug console (on mobile it will automatically open in the bottom right corner when your get errors during development)", "\nOpen this page console: " + currentUrl.toString());
|
24
|
+
}
|
22
25
|
const isMobile = isMobileDevice();
|
23
|
-
if (isMobile)
|
24
|
-
{
|
26
|
+
if (isMobile) {
|
25
27
|
beginWatchingLogs();
|
26
28
|
createConsole(true);
|
27
|
-
if (isMobile)
|
28
|
-
{
|
29
|
+
if (isMobile) {
|
29
30
|
const engineElement = document.querySelector("needle-engine");
|
30
31
|
// setTimeout(() => {
|
31
32
|
// const el = getConsoleElement();
|
@@ -192,6 +193,7 @@
|
|
192
193
|
consoleHtmlElement?.prepend(styles);
|
193
194
|
if (startHidden === true)
|
194
195
|
hideDebugConsole();
|
196
|
+
console.log("🌵 Debug console has loaded");
|
195
197
|
}
|
196
198
|
|
197
199
|
};
|
@@ -796,7 +796,11 @@
|
|
796
796
|
|
797
797
|
this._dispatchReadyAfterFrame = true;
|
798
798
|
const res = ContextRegistry.dispatchCallback(ContextEvent.ContextCreated, this, { files: loadedFiles });
|
799
|
-
if (res)
|
799
|
+
if (res) {
|
800
|
+
if("internalSetLoadingMessage" in this.domElement && typeof this.domElement.internalSetLoadingMessage === "function")
|
801
|
+
this.domElement?.internalSetLoadingMessage("finish loading");
|
802
|
+
await res;
|
803
|
+
}
|
800
804
|
|
801
805
|
this._isCreating = false;
|
802
806
|
if (!this.isManagedExternally)
|
@@ -32,15 +32,17 @@
|
|
32
32
|
|
33
33
|
type LoadingAttributes = {
|
34
34
|
"loading-style"?: "light" | "dark",
|
35
|
-
/**
|
35
|
+
/** Pro feature */
|
36
|
+
"hide-loading-overlay"?: boolean,
|
37
|
+
/** Pro feature */
|
36
38
|
"loading-background-color"?: string,
|
37
|
-
/**
|
39
|
+
/** Pro feature */
|
38
40
|
"loading-logo-src"?: string,
|
39
|
-
/**
|
41
|
+
/** Pro feature */
|
40
42
|
"primary-color"?: string,
|
41
|
-
/**
|
43
|
+
/** Pro feature */
|
42
44
|
"secondary-color"?: string,
|
43
|
-
/**
|
45
|
+
/** Pro feature */
|
44
46
|
"loading-text-color"?: string,
|
45
47
|
}
|
46
48
|
|
@@ -184,7 +184,7 @@
|
|
184
184
|
else
|
185
185
|
loadingStyle = "light";
|
186
186
|
}
|
187
|
-
|
187
|
+
|
188
188
|
const hasLicense = hasProLicense();
|
189
189
|
if (!existing) {
|
190
190
|
this._loadingElement.style.position = "absolute";
|
@@ -244,18 +244,22 @@
|
|
244
244
|
logo.style.width = `${logoSize}px`;
|
245
245
|
logo.style.height = `${logoSize}px`;
|
246
246
|
logo.style.marginBottom = "20px";
|
247
|
-
logo.addEventListener("click", () => window.open("https://needle.tools", "_blank"));
|
248
|
-
logo.style.cursor = "pointer";
|
249
247
|
logo.style.userSelect = "none";
|
250
|
-
logo.style.pointerEvents = "all";
|
251
248
|
logo.style.objectFit = "contain";
|
252
249
|
logo.src = logoSVG;
|
250
|
+
let isUsingCustomLogo = false;
|
253
251
|
if (hasLicense && this._element) {
|
254
252
|
const customLogo = this._element.getAttribute("loading-logo-src");
|
255
253
|
if (customLogo) {
|
254
|
+
isUsingCustomLogo = true;
|
256
255
|
logo.src = customLogo;
|
257
256
|
}
|
258
257
|
}
|
258
|
+
if (!isUsingCustomLogo) {
|
259
|
+
logo.style.cursor = "pointer";
|
260
|
+
logo.style.pointerEvents = "all";
|
261
|
+
logo.addEventListener("click", () => window.open("https://needle.tools", "_blank"));
|
262
|
+
}
|
259
263
|
this._loadingElement.appendChild(logo);
|
260
264
|
this._loadingElement.appendChild(loadingBarContainer);
|
261
265
|
|
@@ -7,9 +7,7 @@
|
|
7
7
|
import { getLoader, registerLoader } from "../engine/engine_gltf.js";
|
8
8
|
import { NeedleGltfLoader } from "./engine_scenetools.js";
|
9
9
|
import { INeedleEngineComponent, LoadedGLTF } from "./engine_types.js";
|
10
|
-
import { isLocalNetwork } from "./engine_networking_utils.js";
|
11
10
|
import { isDevEnvironment, showBalloonWarning } from "./debug/index.js";
|
12
|
-
import { destroy } from "./engine_gameobject.js";
|
13
11
|
import { hasCommercialLicense } from "./engine_license.js";
|
14
12
|
import { VERSION } from "./engine_constants.js";
|
15
13
|
|
@@ -40,7 +38,7 @@
|
|
40
38
|
|
41
39
|
// https://developers.google.com/web/fundamentals/web-components/customelements
|
42
40
|
/** <needle-engine> web component. See @type {NeedleEngineAttributes} attributes for supported attributes
|
43
|
-
* @type {import ("./engine_element_attributes").NeedleEngineAttributes}
|
41
|
+
* @type {import ("./engine_element_attributes.js").NeedleEngineAttributes}
|
44
42
|
*/
|
45
43
|
export class NeedleEngineHTMLElement extends HTMLElement implements INeedleEngineComponent {
|
46
44
|
|
@@ -286,6 +284,13 @@
|
|
286
284
|
},
|
287
285
|
cancelable: true
|
288
286
|
}));
|
287
|
+
if (allowOverridingDefaultLoading) {
|
288
|
+
// Handle the <needle-engine hide-loading-overlay> attribute
|
289
|
+
const hideOverlay = this.getAttribute("hide-loading-overlay");
|
290
|
+
if (hideOverlay !== null && hideOverlay !== undefined && hideOverlay !== "0") {
|
291
|
+
useDefaultLoading = false;
|
292
|
+
}
|
293
|
+
}
|
289
294
|
// for local development we allow overriding the loading screen - but we notify the user that it won't work in a deployed environment
|
290
295
|
if (useDefaultLoading === false && !allowOverridingDefaultLoading) {
|
291
296
|
if (!isDevEnvironment())
|
@@ -372,6 +377,10 @@
|
|
372
377
|
private onReady = () => this._loadingView?.onLoadingFinished();
|
373
378
|
private onError = () => this._loadingView?.setMessage("Loading failed!");
|
374
379
|
|
380
|
+
private internalSetLoadingMessage(str: string) {
|
381
|
+
this._loadingView?.setMessage(str);
|
382
|
+
}
|
383
|
+
|
375
384
|
private getSourceFiles(): Array<string> {
|
376
385
|
const src: string | null | string[] = this.getAttribute("src");
|
377
386
|
if (!src) return [];
|
@@ -426,13 +435,18 @@
|
|
426
435
|
this.removeEventListener(eventName, prev as any);
|
427
436
|
}
|
428
437
|
if (typeof code === "string" && code.length > 0) {
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
438
|
+
try {
|
439
|
+
// indirect eval https://esbuild.github.io/content-types/#direct-eval
|
440
|
+
const fn = (0, eval)(code);
|
441
|
+
// const fn = new Function(newValue);
|
442
|
+
if (typeof fn === "function") {
|
443
|
+
this._previouslyRegisteredMap.set(eventName, fn);
|
444
|
+
this.addEventListener(eventName, evt => fn?.call(globalThis, this._context, evt));
|
445
|
+
}
|
435
446
|
}
|
447
|
+
catch (err) {
|
448
|
+
console.error("Error registering event " + eventName + "=\"" + code + "\" failed with the following error:\n", err);
|
449
|
+
}
|
436
450
|
}
|
437
451
|
}
|
438
452
|
|
@@ -1,10 +1,11 @@
|
|
1
1
|
import { AssetReference } from "../engine/engine_addressables.js";
|
2
2
|
import { InputEvents } from "../engine/engine_input.js";
|
3
3
|
import { isLocalNetwork } from "../engine/engine_networking_utils.js";
|
4
|
-
import { getParam, setParamWithoutReload } from "../engine/engine_utils.js";
|
4
|
+
import { delay, getParam, setParamWithoutReload } from "../engine/engine_utils.js";
|
5
5
|
import { serializable } from "../engine/engine_serialization.js";
|
6
6
|
import { Behaviour, GameObject } from "./Component.js";
|
7
7
|
import { registerObservableAttribute } from "../engine/engine_element_extras.js";
|
8
|
+
import { Object3D } from "three";
|
8
9
|
|
9
10
|
const debug = getParam("debugsceneswitcher");
|
10
11
|
|
@@ -29,11 +30,22 @@
|
|
29
30
|
index: number;
|
30
31
|
}
|
31
32
|
|
33
|
+
/** The ISceneEventListener is called by the SceneSwitcher when a scene is loaded or unloaded. It must be added to the root object of your scene */
|
34
|
+
export interface ISceneEventListener {
|
35
|
+
/** Called when the scene is loaded and added */
|
36
|
+
sceneOpened?(): Promise<void>
|
37
|
+
/** Called before the scene is being removed (due to another scene being loaded) */
|
38
|
+
sceneClosing?(): Promise<void>
|
39
|
+
}
|
40
|
+
|
32
41
|
export class SceneSwitcher extends Behaviour {
|
33
42
|
|
34
43
|
@serializable(AssetReference)
|
35
44
|
scenes!: AssetReference[];
|
36
45
|
|
46
|
+
@serializable(AssetReference)
|
47
|
+
loadingScene!: AssetReference;
|
48
|
+
|
37
49
|
/** the url parameter that is set/used to store the currently loaded scene in, set to "" to disable */
|
38
50
|
@serializable()
|
39
51
|
queryParameterName: string = "scene";
|
@@ -86,7 +98,7 @@
|
|
86
98
|
private _preloadScheduler?: PreLoadScheduler;
|
87
99
|
|
88
100
|
awake(): void {
|
89
|
-
if(debug) console.log("SceneSwitcher", this);
|
101
|
+
if (debug) console.log("SceneSwitcher", this);
|
90
102
|
}
|
91
103
|
|
92
104
|
async start() {
|
@@ -135,6 +147,9 @@
|
|
135
147
|
this._preloadScheduler.maxLoadBehind = this.preloadPrevious;
|
136
148
|
this._preloadScheduler.maxConcurrent = this.preloadConcurrent;
|
137
149
|
this._preloadScheduler.begin();
|
150
|
+
|
151
|
+
// Begin loading the loading scene
|
152
|
+
// if (this.loadingScene) this.loadingScene.loadAssetAsync();
|
138
153
|
}
|
139
154
|
|
140
155
|
onDisable(): void {
|
@@ -157,7 +172,7 @@
|
|
157
172
|
const state = _state?.state;
|
158
173
|
if (state && state.startsWith(this.guid)) {
|
159
174
|
const value = state.substr(this.guid.length + 2);
|
160
|
-
if(debug) console.log("PopState", value);
|
175
|
+
if (debug) console.log("PopState", value);
|
161
176
|
await this.trySelectSceneFromValue(value);
|
162
177
|
}
|
163
178
|
}
|
@@ -170,7 +185,7 @@
|
|
170
185
|
private normalizedSwipeThresholdX = 0.1;
|
171
186
|
private _didSwipe: boolean = false;
|
172
187
|
private onPointerMove = (e: any) => {
|
173
|
-
if(!this.useSwipe) return;
|
188
|
+
if (!this.useSwipe) return;
|
174
189
|
if (!this._didSwipe && e.button === 0 && e.pointerType === "touch" && this.context.input.getPointerPressedCount() === 1) {
|
175
190
|
const delta = this.context.input.getPointerPositionDelta(e.button);
|
176
191
|
if (delta) {
|
@@ -294,7 +309,12 @@
|
|
294
309
|
}
|
295
310
|
async __internalSwitchScene(scene: AssetReference): Promise<boolean> {
|
296
311
|
if (this._currentScene) {
|
297
|
-
if(debug) console.log("UNLOAD", scene.uri)
|
312
|
+
if (debug) console.log("UNLOAD", scene.uri)
|
313
|
+
const sceneListener = this.tryGetSceneEventListener(this._currentScene.asset as any as Object3D);
|
314
|
+
if (sceneListener?.sceneClosing) {
|
315
|
+
const res = sceneListener.sceneClosing();
|
316
|
+
if (res instanceof Promise) await res;
|
317
|
+
}
|
298
318
|
this._currentScene.unload();
|
299
319
|
}
|
300
320
|
this._currentScene = undefined;
|
@@ -303,7 +323,9 @@
|
|
303
323
|
try {
|
304
324
|
const loadStartEvt = new CustomEvent<LoadSceneEvent>("loadscene-start", { detail: { scene: scene, switcher: this, index: index } })
|
305
325
|
this.dispatchEvent(loadStartEvt);
|
306
|
-
await
|
326
|
+
await this.onStartLoading();
|
327
|
+
await scene.loadAssetAsync().catch(console.error);
|
328
|
+
await this.onEndLoading();
|
307
329
|
const finishedEvt = new CustomEvent<LoadSceneEvent>("loadscene-finished", { detail: { scene: scene, switcher: this, index: index } });
|
308
330
|
this.dispatchEvent(finishedEvt);
|
309
331
|
if (finishedEvt.defaultPrevented) {
|
@@ -315,11 +337,11 @@
|
|
315
337
|
return false;
|
316
338
|
}
|
317
339
|
if (this._currentIndex === index) {
|
318
|
-
if(debug) console.log("ADD", scene.uri)
|
340
|
+
if (debug) console.log("ADD", scene.uri)
|
319
341
|
this._currentScene = scene;
|
320
342
|
GameObject.add(scene.asset, this.gameObject);
|
321
343
|
if (this.useSceneLighting)
|
322
|
-
this.context.sceneLighting.enable(scene)
|
344
|
+
this.context.sceneLighting.enable(scene);
|
323
345
|
if (this.useHistory && index >= 0) {
|
324
346
|
// take the index as the query parameter value
|
325
347
|
let queryParameterValue = index.toString();
|
@@ -338,6 +360,12 @@
|
|
338
360
|
history.pushState(stateName, "unused", location.href);
|
339
361
|
}
|
340
362
|
}
|
363
|
+
// Call SceneListener opened callback (if a SceneListener is in the scene)
|
364
|
+
const sceneListener = this.tryGetSceneEventListener(scene.asset as any as Object3D);
|
365
|
+
if (sceneListener?.sceneOpened) {
|
366
|
+
const res = sceneListener.sceneOpened();
|
367
|
+
if (res instanceof Promise) await res;
|
368
|
+
}
|
341
369
|
return true;
|
342
370
|
}
|
343
371
|
}
|
@@ -350,7 +378,7 @@
|
|
350
378
|
preload(index: number) {
|
351
379
|
if (index >= 0 && index < this.scenes.length) {
|
352
380
|
const scene = this.scenes[index];
|
353
|
-
if(scene instanceof AssetReference)
|
381
|
+
if (scene instanceof AssetReference)
|
354
382
|
return scene.preload();
|
355
383
|
}
|
356
384
|
return couldNotLoadScenePromise;
|
@@ -378,6 +406,7 @@
|
|
378
406
|
const lowerCaseValue = value.toLowerCase();
|
379
407
|
for (let i = 0; i < this.scenes.length; i++) {
|
380
408
|
const scene = this.scenes[i];
|
409
|
+
if (!scene) continue;
|
381
410
|
if (sceneUriToName(scene.uri).toLowerCase().includes(lowerCaseValue)) {
|
382
411
|
return this.select(i);;
|
383
412
|
}
|
@@ -394,6 +423,54 @@
|
|
394
423
|
|
395
424
|
return couldNotLoadScenePromise;
|
396
425
|
}
|
426
|
+
|
427
|
+
private _lastLoadingScene: AssetReference | undefined = undefined;
|
428
|
+
private _loadingScenePromise: Promise<boolean> | undefined = undefined;
|
429
|
+
private _isCurrentlyLoading: boolean = false;
|
430
|
+
private async onStartLoading() {
|
431
|
+
this._isCurrentlyLoading = true;
|
432
|
+
if (this.loadingScene) {
|
433
|
+
// save the last loading scene reference so that it can be changed at runtime
|
434
|
+
// since we cache the loading promise here
|
435
|
+
if(this._lastLoadingScene !== this.loadingScene) {
|
436
|
+
this._loadingScenePromise = undefined;
|
437
|
+
}
|
438
|
+
this._lastLoadingScene = this.loadingScene;
|
439
|
+
if (!this._loadingScenePromise) {
|
440
|
+
this._loadingScenePromise = this.loadingScene?.loadAssetAsync();
|
441
|
+
}
|
442
|
+
await this._loadingScenePromise;
|
443
|
+
if (this._isCurrentlyLoading && this.loadingScene?.asset) {
|
444
|
+
if (debug) console.log("Add loading scene", this.loadingScene.uri, this.loadingScene.asset)
|
445
|
+
const obj = this.loadingScene.asset as any as Object3D;
|
446
|
+
GameObject.add(obj, this.gameObject);
|
447
|
+
const sceneListener = this.tryGetSceneEventListener(obj);
|
448
|
+
if (sceneListener?.sceneOpened) {
|
449
|
+
const res = sceneListener.sceneOpened();
|
450
|
+
if (res instanceof Promise) await res;
|
451
|
+
}
|
452
|
+
}
|
453
|
+
}
|
454
|
+
}
|
455
|
+
private async onEndLoading() {
|
456
|
+
this._isCurrentlyLoading = false;
|
457
|
+
if (this.loadingScene?.asset) {
|
458
|
+
if (debug) console.log("Remove loading scene", this.loadingScene.uri);
|
459
|
+
const obj = this.loadingScene.asset as any as Object3D;
|
460
|
+
// try to find an ISceneEventListener component
|
461
|
+
const sceneListener = this.tryGetSceneEventListener(obj);
|
462
|
+
if (typeof sceneListener?.sceneClosing === "function") {
|
463
|
+
const res = sceneListener.sceneClosing();
|
464
|
+
if (res instanceof Promise) await res;
|
465
|
+
}
|
466
|
+
GameObject.remove(obj);
|
467
|
+
}
|
468
|
+
}
|
469
|
+
|
470
|
+
private tryGetSceneEventListener(obj: Object3D): ISceneEventListener | null {
|
471
|
+
const sceneListener = GameObject.foreachComponent(obj, c => (c as any as ISceneEventListener).sceneClosing ? c : null) as ISceneEventListener | null;
|
472
|
+
return sceneListener;
|
473
|
+
}
|
397
474
|
}
|
398
475
|
|
399
476
|
|
@@ -187,6 +187,11 @@
|
|
187
187
|
if (!this.__didAwake) return;
|
188
188
|
if (!this.sprite?.spriteSheet?.sprites) return;
|
189
189
|
const sprite = this.sprite.spriteSheet.sprites[this.spriteIndex];
|
190
|
+
if (!sprite) {
|
191
|
+
if (debug)
|
192
|
+
console.warn("Sprite not found", this.spriteIndex, this.sprite.spriteSheet.sprites);
|
193
|
+
return;
|
194
|
+
}
|
190
195
|
if (!this._currentSprite) {
|
191
196
|
const mat = new THREE.MeshBasicMaterial({ color: 0xffffff, side: THREE.DoubleSide });
|
192
197
|
if (!mat) return;
|