@@ -25,10 +25,8 @@
|
|
25
25
|
config(viteConfig) {
|
26
26
|
// console.log("Update vite defines -------------------------------------------");
|
27
27
|
if (!viteConfig.define) viteConfig.define = {};
|
28
|
-
viteConfig.define.
|
29
|
-
|
30
|
-
generator: needleEngineConfig?.generator,
|
31
|
-
}
|
28
|
+
viteConfig.define.NEEDLE_ENGINE_VERSION = "\"" + tryGetNeedleEngineVersion() + "\"";
|
29
|
+
viteConfig.define.NEEDLE_ENGINE_GENERATOR = "\"" + needleEngineConfig.generator + "\"";
|
32
30
|
|
33
31
|
if (useRapier && userSettings?.useRapier !== true) {
|
34
32
|
const meta = loadConfig();
|
@@ -117,6 +117,20 @@
|
|
117
117
|
// make sure the dependency is installed
|
118
118
|
const depPath = path.join(projectDir, "node_modules", key);
|
119
119
|
if (!existsSync(depPath)) {
|
120
|
+
const val = packageJson.dependencies[key];
|
121
|
+
if (val.startsWith("file:")) {
|
122
|
+
// check if the local path exists:
|
123
|
+
const dirPath = val.substr(5);
|
124
|
+
// check first the value in case it's a absolute path
|
125
|
+
if (!existsSync(dirPath)) {
|
126
|
+
// then check concatenated with the project dir in case it's a relative path
|
127
|
+
const relPath = path.join(projectDir, val.substr(5));
|
128
|
+
if (!existsSync(relPath)) {
|
129
|
+
// if neither directories exist then the local path is invalid and we ignore it
|
130
|
+
continue;
|
131
|
+
}
|
132
|
+
}
|
133
|
+
}
|
120
134
|
log("Dependency not installed", key)
|
121
135
|
return true;
|
122
136
|
}
|
@@ -1,17 +1,43 @@
|
|
1
1
|
import { needleDefines } from "./defines.js";
|
2
|
+
export { needleDefines } from "./defines.js";
|
3
|
+
|
2
4
|
import { needleBuild } from "./build.js";
|
5
|
+
export { needleBuild } from "./build.js";
|
6
|
+
|
3
7
|
import { needleMeta } from "./meta.js"
|
8
|
+
export { needleMeta } from "./meta.js"
|
9
|
+
|
4
10
|
import { needlePoster } from "./poster.js"
|
11
|
+
export { needlePoster } from "./poster.js"
|
12
|
+
|
5
13
|
import { needleReload } from "./reload.js"
|
14
|
+
export { needleReload } from "./reload.js"
|
15
|
+
|
6
16
|
import { needleDrop } from "./drop.js";
|
17
|
+
export { needleDrop } from "./drop.js";
|
18
|
+
|
7
19
|
import { editorConnection } from "./editor-connection.js";
|
20
|
+
export { editorConnection } from "./editor-connection.js";
|
21
|
+
|
8
22
|
import { needleCopyFiles } from "./copyfiles.js";
|
23
|
+
export { needleCopyFiles } from "./copyfiles.js";
|
24
|
+
|
9
25
|
import { needleViteAlias } from "./alias.js";
|
26
|
+
export { needleViteAlias } from "./alias.js";
|
27
|
+
|
10
28
|
import { needleTransformCodegen } from "./transform-codegen.js";
|
29
|
+
export { needleTransformCodegen } from "./transform-codegen.js";
|
30
|
+
|
11
31
|
import { needleLicense } from "./license.js";
|
32
|
+
export { needleLicense } from "./license.js";
|
33
|
+
|
12
34
|
import { needlePeerjs } from "./peer.js";
|
35
|
+
export { needlePeerjs } from "./peer.js";
|
36
|
+
|
13
37
|
import { needleDependencyWatcher } from "./dependency-watcher.js";
|
38
|
+
export { needleDependencyWatcher } from "./dependency-watcher.js";
|
14
39
|
|
40
|
+
|
15
41
|
export * from "./gzip.js";
|
16
42
|
export * from "./config.js";
|
17
43
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
|
2
2
|
async function generatePoster() {
|
3
|
-
const { screenshot } = await import("@needle-tools/engine
|
3
|
+
const { screenshot } = await import("@needle-tools/engine");
|
4
4
|
|
5
5
|
try {
|
6
6
|
const needleEngine = document.querySelector("needle-engine");
|
@@ -303,7 +303,7 @@
|
|
303
303
|
const originalFilePath = filePath;
|
304
304
|
|
305
305
|
// default import path when outside package
|
306
|
-
let importPath = "@needle-tools/engine
|
306
|
+
let importPath = "@needle-tools/engine";
|
307
307
|
|
308
308
|
if (filePath.includes("package~/engine")) {
|
309
309
|
// convert local dev path to project node_modules path
|
@@ -48,4 +48,4 @@
|
|
48
48
|
|
49
49
|
export { InstancingUtil } from "./engine_instancing";
|
50
50
|
export { validate, prefix } from "./engine_util_decorator"
|
51
|
-
export { hasProLicense } from "./engine_license";
|
51
|
+
export { hasProLicense, hasIndieLicense } from "./engine_license";
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { OrbitControls } from "./OrbitControls";
|
2
|
-
import { addNewComponent } from "../engine/engine_components";
|
2
|
+
import { addNewComponent, getOrAddComponent } from "../engine/engine_components";
|
3
3
|
import { Object3D } from "three";
|
4
4
|
import { ICamera } from "../engine/engine_types";
|
5
5
|
import { RGBAColor } from "./js-extensions/RGBAColor";
|
@@ -7,8 +7,12 @@
|
|
7
7
|
import { getCameraController } from "../engine/engine_camera";
|
8
8
|
import { Camera } from "./Camera";
|
9
9
|
import { NeedleEngineHTMLElement } from "../engine/engine_element";
|
10
|
+
import { getParam } from "../engine/engine_utils";
|
10
11
|
|
12
|
+
const debug = getParam("debugmissingcamera");
|
13
|
+
|
11
14
|
ContextRegistry.registerCallback(ContextEvent.MissingCamera, (evt) => {
|
15
|
+
if (debug) console.warn("Creating missing camera")
|
12
16
|
const scene = evt.context.scene;
|
13
17
|
const srcId = "unknown";
|
14
18
|
|
@@ -46,11 +50,14 @@
|
|
46
50
|
const cam = evt.context.mainCameraComponent;
|
47
51
|
const cameraObject = cam?.gameObject;
|
48
52
|
if (cameraObject) {
|
49
|
-
const orbit =
|
53
|
+
const orbit = getOrAddComponent(cameraObject, OrbitControls) as OrbitControls;
|
50
54
|
orbit.sourceId = "unknown";
|
51
|
-
|
52
|
-
|
53
|
-
|
55
|
+
const ctx = evt.context;
|
56
|
+
const fitCamera = () => {
|
57
|
+
ctx.pre_render_callbacks.splice(ctx.pre_render_callbacks.indexOf(fitCamera), 1);
|
58
|
+
orbit.fitCamera();
|
59
|
+
}
|
60
|
+
ctx.pre_render_callbacks.push(fitCamera);
|
54
61
|
}
|
55
62
|
else {
|
56
63
|
console.warn("Missing camera object, can not add orbit controls")
|
@@ -518,6 +518,7 @@
|
|
518
518
|
if (this.__destroyed) return;
|
519
519
|
this.__destroyed = true;
|
520
520
|
this.destroy?.call(this);
|
521
|
+
this.dispatchEvent(new CustomEvent("destroyed", { detail: this }));
|
521
522
|
destroyComponentInstance(this as any);
|
522
523
|
}
|
523
524
|
|
@@ -8,7 +8,7 @@
|
|
8
8
|
const hide = getParam("noerrors");
|
9
9
|
|
10
10
|
const arContainerClassName = "ar";
|
11
|
-
const
|
11
|
+
const globalErrorContainerKey = "needle_engine_global_error_container";
|
12
12
|
const locationRegex = new RegExp(" at .+\/(.+?\.ts)", "g");
|
13
13
|
|
14
14
|
export enum LogType {
|
@@ -24,7 +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
|
+
if (debug) console.log("Is this a local network?", isLocal);
|
28
28
|
if (isLocal) {
|
29
29
|
if (debug)
|
30
30
|
console.log(window.location.hostname);
|
@@ -82,12 +82,17 @@
|
|
82
82
|
if (Array.isArray(message)) {
|
83
83
|
let newMessage = "";
|
84
84
|
for (let i = 0; i < message.length; i++) {
|
85
|
-
|
85
|
+
let msg = message[i];
|
86
|
+
if (msg instanceof Error) {
|
87
|
+
msg = msg.message;
|
88
|
+
}
|
89
|
+
if (typeof msg === "object") continue;
|
86
90
|
if (i > 0) newMessage += " ";
|
87
|
-
newMessage +=
|
91
|
+
newMessage += msg;
|
88
92
|
}
|
89
93
|
message = newMessage;
|
90
94
|
}
|
95
|
+
if (message.length <= 0) return;
|
91
96
|
showMessage(type, domElement, message);
|
92
97
|
}
|
93
98
|
|
@@ -113,7 +118,7 @@
|
|
113
118
|
returnMessageContainer(last as HTMLElement);
|
114
119
|
}
|
115
120
|
// truncate long messages before they go into the cache/set
|
116
|
-
if(msg.length > 300) msg = msg.substring(0, 300) + "...";
|
121
|
+
if (msg.length > 300) msg = msg.substring(0, 300) + "...";
|
117
122
|
if (currentMessages.has(msg)) return;
|
118
123
|
currentMessages.add(msg);
|
119
124
|
const msgcontainer = getMessageContainer(type, msg);
|
@@ -165,10 +170,15 @@
|
|
165
170
|
`;
|
166
171
|
|
167
172
|
function getLogsContainer(domElement: HTMLElement): HTMLElement {
|
168
|
-
if (
|
169
|
-
|
173
|
+
if (!globalThis[globalErrorContainerKey]) {
|
174
|
+
globalThis[globalErrorContainerKey] = new Map<HTMLElement, HTMLElement>();
|
175
|
+
}
|
176
|
+
const errorsMap = globalThis[globalErrorContainerKey] as Map<HTMLElement, HTMLElement>;
|
177
|
+
if (errorsMap.has(domElement)) {
|
178
|
+
return errorsMap.get(domElement)!;
|
170
179
|
} else {
|
171
180
|
const container = document.createElement("div");
|
181
|
+
errorsMap.set(domElement, container);
|
172
182
|
container.setAttribute("data-needle_engine_debug_overlay", "");
|
173
183
|
container.classList.add(arContainerClassName);
|
174
184
|
container.classList.add("desktop");
|
@@ -190,7 +200,6 @@
|
|
190
200
|
container.style.overflow = "auto";
|
191
201
|
// container.style.border = "1px solid red";
|
192
202
|
domElement.appendChild(container);
|
193
|
-
errorContainer.set(domElement, container);
|
194
203
|
|
195
204
|
const style = document.createElement("style");
|
196
205
|
style.innerHTML = logsContainerStyles;
|
@@ -1,7 +1,17 @@
|
|
1
|
-
declare const
|
2
|
-
|
3
|
-
|
1
|
+
declare const NEEDLE_ENGINE_VERSION: string;
|
2
|
+
declare const NEEDLE_ENGINE_GENERATOR: string;
|
3
|
+
// https://esbuild.github.io/content-types/#direct-eval
|
4
|
+
(0, eval)(`
|
5
|
+
if(!globalThis["NEEDLE_ENGINE_VERSION"])
|
6
|
+
globalThis["NEEDLE_ENGINE_VERSION"] = "0.0.0";
|
7
|
+
if(!globalThis["NEEDLE_ENGINE_GENERATOR"])
|
8
|
+
globalThis["NEEDLE_ENGINE_GENERATOR"] = "unknown";
|
9
|
+
`);
|
4
10
|
|
11
|
+
export const VERSION = NEEDLE_ENGINE_VERSION;
|
12
|
+
export const GENERATOR = NEEDLE_ENGINE_GENERATOR;
|
13
|
+
|
14
|
+
|
5
15
|
export const activeInHierarchyFieldName = "needle_isActiveInHierarchy";
|
6
16
|
export const builtinComponentKeyName = "builtin_components";
|
7
17
|
// It's easier to use a string than a symbol here because the symbol might not be the same when imported in other packages
|
@@ -17,22 +17,30 @@
|
|
17
17
|
|
18
18
|
export type ContextCallback = (evt: ContextEventArgs) => void | Promise<any> | IComponent;
|
19
19
|
|
20
|
+
/** Use to register to various Needle Engine context events and to get access to all current instances
|
21
|
+
* e.g. when being created in the DOM
|
22
|
+
* */
|
20
23
|
export class ContextRegistry {
|
21
24
|
|
25
|
+
/** The currently active (rendering) Needle Engine context */
|
22
26
|
static get Current(): IContext {
|
23
27
|
return globalThis["NeedleEngine.Context.Current"]
|
24
28
|
}
|
29
|
+
/** @internal */
|
25
30
|
static set Current(ctx: IContext) {
|
26
31
|
globalThis["NeedleEngine.Context.Current"] = ctx;
|
27
32
|
}
|
28
33
|
|
34
|
+
/** All currently registered Needle Engine contexts. Do not modify */
|
29
35
|
static Registered: IContext[] = [];
|
30
36
|
|
37
|
+
/** @internal Internal use only */
|
31
38
|
static register(ctx: IContext) {
|
32
39
|
this.Registered.push(ctx);
|
33
40
|
this.dispatchCallback(ContextEvent.ContextRegistered, ctx);
|
34
41
|
}
|
35
42
|
|
43
|
+
/** @internal Internal use only */
|
36
44
|
static unregister(ctx: IContext) {
|
37
45
|
const index = this.Registered.indexOf(ctx);
|
38
46
|
if (index === -1) return;
|
@@ -53,6 +61,7 @@
|
|
53
61
|
this._callbacks[evt].splice(index, 1);
|
54
62
|
}
|
55
63
|
|
64
|
+
/** @internal */
|
56
65
|
static dispatchCallback(evt: ContextEvent, context: IContext) {
|
57
66
|
if (!this._callbacks[evt]) return true;
|
58
67
|
const args = { event: evt, context }
|
@@ -70,4 +79,6 @@
|
|
70
79
|
static addContextDestroyedCallback(callback: ContextCallback) {
|
71
80
|
this.registerCallback(ContextEvent.ContextDestroyed, callback);
|
72
81
|
}
|
73
|
-
}
|
82
|
+
}
|
83
|
+
|
84
|
+
export { ContextRegistry as NeedleEngine };
|
@@ -29,8 +29,8 @@
|
|
29
29
|
import { destroy, foreachComponent } from './engine_gameobject';
|
30
30
|
import { ContextEvent, ContextRegistry } from './engine_context_registry';
|
31
31
|
import { delay } from './engine_utils';
|
32
|
-
import {
|
33
|
-
|
32
|
+
import { VERSION } from './engine_constants';
|
33
|
+
import { isDevEnvironment, LogType, showBalloonMessage } from './debug';
|
34
34
|
|
35
35
|
|
36
36
|
const debug = utils.getParam("debugSetup");
|
@@ -96,7 +96,7 @@
|
|
96
96
|
|
97
97
|
/** the needle engine version */
|
98
98
|
get version() {
|
99
|
-
return
|
99
|
+
return VERSION;
|
100
100
|
}
|
101
101
|
|
102
102
|
static get Current(): Context {
|
@@ -245,6 +245,7 @@
|
|
245
245
|
|
246
246
|
private _sizeChanged: boolean = false;
|
247
247
|
private _isCreated: boolean = false;
|
248
|
+
private _isCreating: boolean = false;
|
248
249
|
private _isVisible: boolean = false;
|
249
250
|
|
250
251
|
private _stats: Stats.default | null = stats ? Stats.default() : null;
|
@@ -262,7 +263,7 @@
|
|
262
263
|
const params: WebGLRendererParameters = {
|
263
264
|
antialias: true,
|
264
265
|
};
|
265
|
-
|
266
|
+
|
266
267
|
// get canvas already configured in the Needle Engine Web Component
|
267
268
|
const canvas = args?.domElement?.shadowRoot?.querySelector("canvas");
|
268
269
|
if (canvas) params.canvas = canvas;
|
@@ -361,19 +362,37 @@
|
|
361
362
|
}
|
362
363
|
|
363
364
|
async onCreate(buildScene?: (context: Context, loadingOptions?: LoadingOptions) => Promise<void>, opts?: LoadingOptions) {
|
364
|
-
|
365
|
-
|
366
|
-
|
365
|
+
try {
|
366
|
+
this._isCreating = true;
|
367
|
+
const res = await this.internalOnCreate(buildScene, opts);
|
368
|
+
return res;
|
367
369
|
}
|
368
|
-
|
369
|
-
|
370
|
-
|
370
|
+
finally {
|
371
|
+
this._isCreating = false;
|
372
|
+
}
|
371
373
|
}
|
372
374
|
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
375
|
+
/** Will destroy all scenes and objects in the scene
|
376
|
+
*/
|
377
|
+
clear() {
|
378
|
+
if (this.scene?.children.length) {
|
379
|
+
destroy(this.scene, true, true);
|
380
|
+
this.scene = new Scene();
|
381
|
+
}
|
382
|
+
if (this.renderer) {
|
383
|
+
this.renderer.setClearAlpha(0);
|
384
|
+
this.renderer.clear();
|
385
|
+
}
|
386
|
+
}
|
387
|
+
|
388
|
+
dispose() {
|
389
|
+
this.internalOnDestroy();
|
390
|
+
}
|
391
|
+
|
392
|
+
/**@deprecated use dispose() */
|
393
|
+
onDestroy() { this.internalOnDestroy(); }
|
394
|
+
private internalOnDestroy() {
|
395
|
+
this.clear();
|
377
396
|
this.renderer?.setAnimationLoop(null);
|
378
397
|
if (!this.isManagedExternally) {
|
379
398
|
this.renderer?.dispose();
|
@@ -537,6 +556,13 @@
|
|
537
556
|
|
538
557
|
private async internalOnCreate(buildScene?: (context: Context, opts?: LoadingOptions) => Promise<void>, opts?: LoadingOptions) {
|
539
558
|
|
559
|
+
this.clear();
|
560
|
+
// stop the animation loop if its running during creation
|
561
|
+
// since we do not want to start enabling scripts etc before they are deserialized
|
562
|
+
this.renderer?.setAnimationLoop(null);
|
563
|
+
|
564
|
+
await delay(1);
|
565
|
+
|
540
566
|
Context.Current = this;
|
541
567
|
await ContextRegistry.dispatchCallback(ContextEvent.ContextCreationStart, this);
|
542
568
|
|
@@ -646,18 +672,56 @@
|
|
646
672
|
this.domElement.appendChild(this._stats.dom);
|
647
673
|
}
|
648
674
|
|
649
|
-
this.renderer.setAnimationLoop(this.render.bind(this));
|
650
|
-
|
651
675
|
if (debug)
|
652
676
|
logHierarchy(this.scene, true);
|
653
677
|
|
678
|
+
this._isCreating = false;
|
679
|
+
this.restartRenderLoop();
|
680
|
+
this._dispatchReadyAfterFrame = true;
|
654
681
|
return ContextRegistry.dispatchCallback(ContextEvent.ContextCreated, this);
|
655
682
|
}
|
656
683
|
|
657
684
|
private _accumulatedTime = 0;
|
658
685
|
private _framerateClock = new Clock();
|
686
|
+
private _dispatchReadyAfterFrame = false;
|
659
687
|
|
660
|
-
|
688
|
+
/** Sets the animation loop.
|
689
|
+
* Can not be done while creating the context or when disposed
|
690
|
+
**/
|
691
|
+
public restartRenderLoop(): boolean {
|
692
|
+
if (!this.renderer) {
|
693
|
+
console.error("Can not start render loop without renderer");
|
694
|
+
return false;
|
695
|
+
}
|
696
|
+
if (this._isCreating) {
|
697
|
+
console.warn("Can not start render loop while creating context");
|
698
|
+
return false;
|
699
|
+
}
|
700
|
+
const renderMethod = this.render.bind(this);
|
701
|
+
this.renderer.setAnimationLoop(renderMethod);
|
702
|
+
return true;
|
703
|
+
}
|
704
|
+
|
705
|
+
private render(_, frame: XRFrame | null) {
|
706
|
+
if (isDevEnvironment() || debug || looputils.hasNewScripts()) {
|
707
|
+
try {
|
708
|
+
this.internalRender(_, frame);
|
709
|
+
}
|
710
|
+
catch (err) {
|
711
|
+
if ((isDevEnvironment() || debug) && err instanceof Error)
|
712
|
+
showBalloonMessage("Exception during render-loop.<br/>See console for details.", LogType.Error);
|
713
|
+
console.error(err);
|
714
|
+
console.warn("Stopping render loop due to error")
|
715
|
+
this.renderer.setAnimationLoop(null);
|
716
|
+
this.domElement.dispatchEvent(new CustomEvent("error", { detail: err }));
|
717
|
+
}
|
718
|
+
}
|
719
|
+
else {
|
720
|
+
this.internalRender(_, frame);
|
721
|
+
}
|
722
|
+
}
|
723
|
+
|
724
|
+
private internalRender(_, frame: XRFrame | null) {
|
661
725
|
this._xrFrame = frame;
|
662
726
|
|
663
727
|
this._currentFrameEvent = FrameEvent.Undefined;
|
@@ -750,7 +814,7 @@
|
|
750
814
|
}
|
751
815
|
this.physics.engine.postStep();
|
752
816
|
}
|
753
|
-
|
817
|
+
|
754
818
|
if (this.onHandlePaused()) return;
|
755
819
|
|
756
820
|
if (this.isVisibleToUser) {
|
@@ -812,7 +876,8 @@
|
|
812
876
|
|
813
877
|
this._stats?.end();
|
814
878
|
|
815
|
-
if (this.
|
879
|
+
if (this._dispatchReadyAfterFrame) {
|
880
|
+
this._dispatchReadyAfterFrame = false;
|
816
881
|
this.domElement.dispatchEvent(new CustomEvent("ready"));
|
817
882
|
}
|
818
883
|
}
|
@@ -22,6 +22,9 @@
|
|
22
22
|
"dracoDecoderType"?: string;
|
23
23
|
/** override the default ktx2 decoder path */
|
24
24
|
"ktx2DecoderPath"?: string;
|
25
|
+
|
26
|
+
addEventListener(event: "ready", callback: (event: CustomEvent) => void): void;
|
27
|
+
addEventListener(event: "error", callback: (event: CustomEvent) => void): void;
|
25
28
|
}
|
26
29
|
|
27
30
|
type LoadingAttributes = {
|
@@ -38,5 +41,17 @@
|
|
38
41
|
"loading-text-color"?: string,
|
39
42
|
}
|
40
43
|
|
44
|
+
type SkyboxAttributes = {
|
45
|
+
/** URL to .exr, .hdr, .png, .jpg to be used as skybox */
|
46
|
+
"skybox-image"?: string,
|
47
|
+
/** URL to .exr, .hdr, .png, .jpg to be used as skybox */
|
48
|
+
"environment-image"?: string,
|
49
|
+
}
|
41
50
|
|
42
|
-
|
51
|
+
|
52
|
+
export type NeedleEngineAttributes =
|
53
|
+
MainAttributes
|
54
|
+
& Partial<Omit<HTMLElement, "style">>
|
55
|
+
& LoadingAttributes
|
56
|
+
& SkyboxAttributes
|
57
|
+
;
|
@@ -19,6 +19,7 @@
|
|
19
19
|
onLoadingBegin(message?: string);
|
20
20
|
onLoadingUpdate(progress: LoadingProgressArgs | number, message?: string);
|
21
21
|
onLoadingFinished(message?: string);
|
22
|
+
setMessage(string:string);
|
22
23
|
}
|
23
24
|
|
24
25
|
let currentFileProgress = 0;
|
@@ -1,18 +1,16 @@
|
|
1
|
-
import { Context,
|
1
|
+
import { Context, LoadingProgressArgs } from "./engine_setup";
|
2
2
|
import { AROverlayHandler, arContainerClassName } from "./engine_element_overlay";
|
3
3
|
import { GameObject } from "../engine-components/Component";
|
4
|
-
import { processNewScripts } from "./engine_mainloop_utils";
|
5
4
|
import { calculateProgress01, EngineLoadingView, ILoadingViewHandler } from "./engine_element_loading";
|
6
|
-
import {
|
5
|
+
import { getParam } from "./engine_utils";
|
7
6
|
import { setDracoDecoderPath, setDracoDecoderType, setKtx2TranscoderPath } from "./engine_loaders";
|
8
7
|
import { getLoader, registerLoader } from "../engine/engine_gltf";
|
9
8
|
import { NeedleGltfLoader } from "./engine_scenetools";
|
10
9
|
import { INeedleEngineComponent } from "./engine_types";
|
11
10
|
import { isLocalNetwork } from "./engine_networking_utils";
|
12
11
|
import { showBalloonWarning } from "./debug";
|
13
|
-
import {
|
12
|
+
import { destroy } from "./engine_gameobject";
|
14
13
|
|
15
|
-
|
16
14
|
//
|
17
15
|
// registering loader here too to make sure it's imported when using engine via vanilla js
|
18
16
|
registerLoader(NeedleGltfLoader);
|
@@ -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 {NeedleEngineAttributes}
|
41
|
+
* @type {import ("./engine_element_attributes").NeedleEngineAttributes}
|
44
42
|
*/
|
45
43
|
export class NeedleEngineHTMLElement extends HTMLElement implements INeedleEngineComponent {
|
46
44
|
|
@@ -95,6 +93,7 @@
|
|
95
93
|
this._context = new Context({ domElement: this });
|
96
94
|
// TODO: do we want to rename this event?
|
97
95
|
this.addEventListener("ready", this.onReady);
|
96
|
+
this.addEventListener("error", this.onError);
|
98
97
|
}
|
99
98
|
|
100
99
|
async connectedCallback() {
|
@@ -104,23 +103,32 @@
|
|
104
103
|
}
|
105
104
|
|
106
105
|
this.onSetupDesktop();
|
107
|
-
|
106
|
+
|
107
|
+
if (!this.getAttribute("src")) {
|
108
|
+
const glob = globalThis["needle:codegen_files"];
|
109
|
+
if (glob)
|
110
|
+
this.setAttribute("src", glob);
|
111
|
+
return;
|
112
|
+
}
|
113
|
+
|
114
|
+
const src = this.getAttribute("src");
|
108
115
|
if (src && src.length > 0) {
|
109
|
-
await this.onLoad(
|
116
|
+
await this.onLoad();
|
110
117
|
}
|
111
118
|
this.onSetupDesktop();
|
112
119
|
}
|
113
120
|
|
114
121
|
disconnectedCallback() {
|
115
|
-
|
122
|
+
if(debug) console.warn("<needle-engine> disconnected");
|
123
|
+
this._context?.dispose();
|
116
124
|
}
|
117
125
|
|
118
126
|
attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
|
119
127
|
// console.log(name, oldValue, newValue);
|
120
128
|
switch (name) {
|
121
129
|
case "src":
|
122
|
-
if (debug) console.
|
123
|
-
this.onLoad(
|
130
|
+
if (debug) console.warn("src changed to type:", typeof newValue, ", from \"", _oldValue, "\" to \"", newValue, "\"")
|
131
|
+
this.onLoad();
|
124
132
|
// this._watcher?.onSourceChanged(newValue);
|
125
133
|
break;
|
126
134
|
case "hash":
|
@@ -154,37 +162,31 @@
|
|
154
162
|
}
|
155
163
|
}
|
156
164
|
|
157
|
-
private
|
165
|
+
private _loadId: number = 0;
|
166
|
+
private _lastSourceFiles: Array<string> | null = null;
|
158
167
|
|
159
|
-
private async onLoad(
|
160
|
-
|
168
|
+
private async onLoad() {
|
169
|
+
|
170
|
+
if (!this._context) {
|
171
|
+
console.error("Needle Engine: Context not initialized");
|
161
172
|
return;
|
162
173
|
}
|
163
|
-
|
174
|
+
|
175
|
+
const filesToLoad = this.getSourceFiles();
|
176
|
+
if (filesToLoad === null || filesToLoad === undefined || filesToLoad.length <= 0) {
|
177
|
+
if (debug) console.warn("Clear scene", filesToLoad);
|
178
|
+
this._context.clear();
|
164
179
|
return;
|
165
180
|
}
|
166
|
-
if (!
|
167
|
-
console.error("Needle Engine: No source specified", src);
|
181
|
+
else if (!this.checkIfSourceHasChanged(filesToLoad, this._lastSourceFiles)) {
|
168
182
|
return;
|
169
183
|
}
|
170
|
-
if (!this._context) {
|
171
|
-
console.error("Needle Engine: Context not initialized");
|
172
|
-
return;
|
173
|
-
}
|
174
184
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
// Set the source attribute so codegen doesnt try to re-assign it again and we communicate to the outside which root files are being loaded
|
179
|
-
this._isCurrentlySettingsSourceAttribute = true;
|
180
|
-
this.setAttribute("src", src?.toString());
|
181
|
-
this._isCurrentlySettingsSourceAttribute = false;
|
182
|
-
|
185
|
+
this._lastSourceFiles = filesToLoad;
|
186
|
+
const loadId = ++this._loadId;
|
183
187
|
const alias = this.getAttribute("alias");
|
184
188
|
this.classList.add("loading");
|
185
|
-
if (debug) console.log("Needle Engine: Begin loading", alias ?? "");
|
186
189
|
|
187
|
-
|
188
190
|
// Loading start events
|
189
191
|
const allowOverridingDefaultLoading = true;
|
190
192
|
// default loading can be overriden by calling preventDefault in the onload start event
|
@@ -199,23 +201,11 @@
|
|
199
201
|
this._loadingView = new EngineLoadingView(this);
|
200
202
|
if (useDefaultLoading)
|
201
203
|
this._loadingView?.onLoadingBegin("begin load");
|
204
|
+
if (debug)
|
205
|
+
console.warn("--------------", loadId, "Needle Engine: Begin loading", alias ?? "", filesToLoad);
|
202
206
|
this.onBeforeBeginLoading();
|
203
207
|
|
204
|
-
if (debug) console.log(
|
205
|
-
let filesToLoad: Array<string>;
|
206
|
-
// When using globalThis the src is an array already
|
207
|
-
if (Array.isArray(src)) {
|
208
|
-
filesToLoad = src;
|
209
|
-
}
|
210
|
-
// When assigned from codegen the src is a stringified array
|
211
|
-
else if (src.startsWith("[") && src.endsWith("]")) {
|
212
|
-
filesToLoad = JSON.parse(src);
|
213
|
-
}
|
214
|
-
// src.toString for an array produces a comma separated list
|
215
|
-
else if (src.includes(",")) {
|
216
|
-
filesToLoad = src.split(",");
|
217
|
-
}
|
218
|
-
else filesToLoad = [src];
|
208
|
+
if (debug) console.log(filesToLoad);
|
219
209
|
|
220
210
|
let loadFunction: any = null;
|
221
211
|
|
@@ -225,6 +215,7 @@
|
|
225
215
|
if (ctx.hash) hash = Number.parseInt(ctx.hash) ?? 0;
|
226
216
|
const loader = getLoader();
|
227
217
|
for (let i = 0; i < filesToLoad.length; i++) {
|
218
|
+
if (this._loadId !== loadId) return;
|
228
219
|
const url = filesToLoad[i];
|
229
220
|
// Check if the url contains glb or gltf, this is mainly to handle legacy projects/codegen files were the src attribute was a function name
|
230
221
|
if (!url.includes(".glb") && !url.includes(".gltf")) {
|
@@ -261,54 +252,81 @@
|
|
261
252
|
this.dispatchEvent(new CustomEvent("progress", progressEventArgs));
|
262
253
|
});
|
263
254
|
const obj = res?.scene;
|
264
|
-
if (obj)
|
255
|
+
if (obj) {
|
256
|
+
// if the loading ID changed but the scene has already been loaded we want to dispose it
|
257
|
+
if (this._loadId !== loadId) {
|
258
|
+
destroy(obj, true, true)
|
259
|
+
return;
|
260
|
+
}
|
261
|
+
GameObject.add(obj, ctx.scene, ctx);
|
262
|
+
}
|
265
263
|
}
|
266
|
-
if (
|
267
|
-
processNewScripts(ctx);
|
264
|
+
if (this._loadId !== loadId) return;
|
268
265
|
};
|
269
266
|
}
|
270
267
|
if (!loadFunction) {
|
271
|
-
console.error("Needle Engine: No files to load",
|
268
|
+
console.error("Needle Engine: No files to load", filesToLoad);
|
272
269
|
return;
|
273
270
|
}
|
274
|
-
|
275
271
|
const currentHash = this.getAttribute("hash");
|
276
272
|
if (currentHash !== null && currentHash !== undefined)
|
277
273
|
this._context.hash = currentHash;
|
278
274
|
this._context.alias = alias;
|
279
|
-
|
280
|
-
if (
|
281
|
-
|
282
|
-
|
283
|
-
else {
|
284
|
-
await loadFunction(this._context);
|
285
|
-
}
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
console.log("Needle Engine: finished loading", alias ?? "")
|
275
|
+
await this._context.onCreate(loadFunction);
|
276
|
+
if (this._loadId !== loadId) return;
|
277
|
+
if (debug)
|
278
|
+
console.warn("--------------", loadId, "Needle Engine: finished loading", alias ?? "", filesToLoad);
|
290
279
|
this._loadingProgress01 = 1;
|
291
280
|
if (useDefaultLoading) {
|
292
281
|
this._loadingView?.onLoadingUpdate(1, "creating scene");
|
293
|
-
if (contextWasCreated) this.onReady();
|
294
282
|
}
|
295
283
|
this.classList.remove("loading");
|
296
284
|
this.classList.add("loading-finished");
|
297
|
-
if (debug)
|
298
|
-
console.log("Needle Engine: finished loading", alias ?? "");
|
299
285
|
this.dispatchEvent(new CustomEvent("loadfinished", {
|
300
286
|
detail: {
|
301
287
|
context: this._context,
|
302
|
-
src: alias
|
288
|
+
src: alias
|
303
289
|
}
|
304
290
|
}));
|
305
|
-
|
306
291
|
}
|
307
292
|
|
308
293
|
/** called by the context when the first frame has been rendered */
|
309
294
|
private onReady = () => this._loadingView?.onLoadingFinished();
|
295
|
+
private onError = () => this._loadingView?.setMessage("Loading failed!");
|
310
296
|
|
297
|
+
private getSourceFiles(): Array<string> | null {
|
298
|
+
const src: string | null | string[] = this.getAttribute("src");
|
299
|
+
if (!src) return null;
|
311
300
|
|
301
|
+
let filesToLoad: Array<string>;
|
302
|
+
// When using globalThis the src is an array already
|
303
|
+
if (Array.isArray(src)) {
|
304
|
+
filesToLoad = src;
|
305
|
+
}
|
306
|
+
// When assigned from codegen the src is a stringified array
|
307
|
+
else if (src.startsWith("[") && src.endsWith("]")) {
|
308
|
+
filesToLoad = JSON.parse(src);
|
309
|
+
}
|
310
|
+
// src.toString for an array produces a comma separated list
|
311
|
+
else if (src.includes(",")) {
|
312
|
+
filesToLoad = src.split(",");
|
313
|
+
}
|
314
|
+
else filesToLoad = [src];
|
315
|
+
return filesToLoad;
|
316
|
+
}
|
317
|
+
|
318
|
+
private checkIfSourceHasChanged(current: Array<string> | null, previous: Array<string> | null): boolean {
|
319
|
+
if (current?.length !== previous?.length) return true;
|
320
|
+
if (current == null && previous !== null) return true;
|
321
|
+
if (current !== null && previous == null) return true;
|
322
|
+
if (current !== null && previous !== null) {
|
323
|
+
for (let i = 0; i < current?.length; i++) {
|
324
|
+
if (current[i] !== previous[i]) return true;
|
325
|
+
}
|
326
|
+
}
|
327
|
+
return false;
|
328
|
+
}
|
329
|
+
|
312
330
|
private registerEventFromAttribute(eventName: string, code: string) {
|
313
331
|
if (typeof code === "string" && code.length > 0) {
|
314
332
|
// indirect eval https://esbuild.github.io/content-types/#direct-eval
|
@@ -53,7 +53,12 @@
|
|
53
53
|
const licenseDelay = 200;
|
54
54
|
|
55
55
|
function onNonCommercialVersionDetected(ctx: IContext) {
|
56
|
-
|
56
|
+
logNonCommercialUse();
|
57
|
+
ctx.domElement.addEventListener("ready", () => {
|
58
|
+
setTimeout(()=>{
|
59
|
+
insertNonCommercialUseHint(ctx);
|
60
|
+
}, 1000);
|
61
|
+
});
|
57
62
|
sendUsageMessageToAnalyticsBackend();
|
58
63
|
}
|
59
64
|
|
@@ -71,9 +76,6 @@
|
|
71
76
|
}
|
72
77
|
}, 100);
|
73
78
|
|
74
|
-
if (!hasCommercialLicense())
|
75
|
-
logNonCommercialUse();
|
76
|
-
|
77
79
|
let svg = `<img class="logo" src="${logoSVG}" style="width: 40px; height: 40px; margin-right: 2px; vertical-align: middle; margin-bottom: 2px;"/>`;
|
78
80
|
const logoElement = document.createElement("div");
|
79
81
|
logoElement.innerHTML = svg;
|
@@ -101,8 +103,11 @@
|
|
101
103
|
}, removeDelay);
|
102
104
|
|
103
105
|
}
|
104
|
-
|
106
|
+
let lastLogTime = 0;
|
105
107
|
async function logNonCommercialUse(_logo?: string) {
|
108
|
+
const now = Date.now();
|
109
|
+
if (now - lastLogTime < 2000) return;
|
110
|
+
lastLogTime = now;
|
106
111
|
const logo = "data:image/webp;base64,UklGRrABAABXRUJQVlA4WAoAAAAQAAAAHwAAHwAAQUxQSKEAAAARN6CmbSM4WR7vdARON11EBDq3fLiNbVtVzpMCPlKAEzsx0Y/x+Ovuv4dn0EFE/ydAvz6YggXzgh5sVgXM/zOC/4sii7qgGvB5N7hmuQYwkvazWAu1JPW41FXSHq6pnaQWvqYH18Fc0j1hO/BFTtIeSBlJi5w6qIIO7IOrwhFsB2Yxukif0FTRLpXswHR8MxbslKe9VZsn/Ub5C7YFOpqSTABWUDgg6AAAAFAGAJ0BKiAAIAA+7VyoTqmkpCI3+qgBMB2JbACdMt69DwMIQBLhkTO6XwY00UEDK6cNIDnuNibPf0EgAP7Y1myuiQHLDsF/0h5unrGh6WAbv7aegg2ZMd3uRKfT/3SJztcaujYfTvMXspfCTmYcoO6a+vhC3ss4M8uM58t4siiu59I4aOl59e9Sr6xoxYlHf2v+NnBNpJYeJf8jABQAId/PXuBkLEFkiCucgSGEcfhvajql/j3reCGl0M5/9gQWy7ayNPs+wlvIxFnNfSlfuND4CZOCyxOHhRqOmHN4ULHo3tCSrUNvgAA=";
|
107
112
|
const style = `
|
108
113
|
font-size: 18px;
|
@@ -14,6 +14,10 @@
|
|
14
14
|
// so we use this copy buffer
|
15
15
|
const new_scripts_buffer: any[] = [];
|
16
16
|
|
17
|
+
export function hasNewScripts() {
|
18
|
+
return new_scripts_buffer.length > 0;
|
19
|
+
}
|
20
|
+
|
17
21
|
export function processNewScripts(context: IContext) {
|
18
22
|
if (context.new_scripts.length <= 0) return;
|
19
23
|
if (debug)
|
@@ -38,8 +38,8 @@
|
|
38
38
|
|
39
39
|
let RAPIER: undefined | any = undefined;
|
40
40
|
declare const NEEDLE_USE_RAPIER: boolean;
|
41
|
+
globalThis["NEEDLE_USE_RAPIER"] = globalThis["NEEDLE_USE_RAPIER"] || true;
|
41
42
|
|
42
|
-
|
43
43
|
if (NEEDLE_USE_RAPIER) {
|
44
44
|
ContextRegistry.registerCallback(ContextEvent.ContextCreationStart, evt => {
|
45
45
|
if (debugPhysics)
|
@@ -448,4 +448,66 @@
|
|
448
448
|
const res = (await fetch("https://api.db-ip.com/v2/free/self").catch(() => null))!;
|
449
449
|
const json = await res.json() as IpAndLocation;
|
450
450
|
return json;
|
451
|
-
}
|
451
|
+
}
|
452
|
+
|
453
|
+
|
454
|
+
|
455
|
+
|
456
|
+
|
457
|
+
declare type AttributeChangeCallback = (value: string | null) => void;
|
458
|
+
declare type HtmlElementExtra = {
|
459
|
+
observer: MutationObserver,
|
460
|
+
attributeChangedListeners: Map<string, Array<AttributeChangeCallback>>,
|
461
|
+
}
|
462
|
+
const mutationObserverMap = new WeakMap<HTMLElement, HtmlElementExtra>();
|
463
|
+
|
464
|
+
/** Register a callback when a attribute changes */
|
465
|
+
export function addAttributeChangeCallback(domElement: HTMLElement, name: string, callback: AttributeChangeCallback) {
|
466
|
+
if (!mutationObserverMap.get(domElement)) {
|
467
|
+
const observer = new MutationObserver((mutations) => {
|
468
|
+
handleMutations(domElement, mutations);
|
469
|
+
});
|
470
|
+
mutationObserverMap.set(domElement, {
|
471
|
+
observer,
|
472
|
+
attributeChangedListeners: new Map<string, Array<AttributeChangeCallback>>(),
|
473
|
+
});
|
474
|
+
observer.observe(domElement, { attributes: true });
|
475
|
+
}
|
476
|
+
|
477
|
+
const listeners = mutationObserverMap.get(domElement)!.attributeChangedListeners;
|
478
|
+
if (!listeners.has(name)) {
|
479
|
+
listeners.set(name, []);
|
480
|
+
}
|
481
|
+
listeners.get(name)!.push(callback);
|
482
|
+
};
|
483
|
+
export function removeAttributeChangeCallback(domElement: HTMLElement, name: string, callback: AttributeChangeCallback) {
|
484
|
+
if (!mutationObserverMap.get(domElement)) return;
|
485
|
+
const listeners = mutationObserverMap.get(domElement)!.attributeChangedListeners;
|
486
|
+
if (!listeners.has(name)) return;
|
487
|
+
const arr = listeners.get(name);
|
488
|
+
const index = arr!.indexOf(callback);
|
489
|
+
if (index === -1) return;
|
490
|
+
arr!.splice(index, 1);
|
491
|
+
if (arr!.length <= 0) {
|
492
|
+
listeners.delete(name);
|
493
|
+
const entry = mutationObserverMap.get(domElement);
|
494
|
+
entry?.observer.disconnect();
|
495
|
+
mutationObserverMap.delete(domElement);
|
496
|
+
}
|
497
|
+
}
|
498
|
+
|
499
|
+
function handleMutations(domElement: HTMLElement, mutations: Array<MutationRecord>) {
|
500
|
+
const listeners = mutationObserverMap.get(domElement)!.attributeChangedListeners;
|
501
|
+
for (const mut of mutations) {
|
502
|
+
if (mut.type === "attributes") {
|
503
|
+
const name = mut.attributeName!;
|
504
|
+
const value = domElement.getAttribute(name);
|
505
|
+
if (listeners.has(name)) {
|
506
|
+
for (const listener of listeners.get(name)!) {
|
507
|
+
listener(value);
|
508
|
+
}
|
509
|
+
}
|
510
|
+
}
|
511
|
+
}
|
512
|
+
|
513
|
+
}
|
@@ -1,8 +1,6 @@
|
|
1
1
|
import { makeErrorsVisibleForDevelopment } from "./engine/debug/debug_overlay";
|
2
2
|
makeErrorsVisibleForDevelopment();
|
3
3
|
|
4
|
-
import { NEEDLE_ENGINE_GENERATOR, NEEDLE_ENGINE_VERSION } from "./engine/engine_constants";
|
5
|
-
|
6
4
|
import "./engine/engine_element";
|
7
5
|
import "./engine/engine_setup";
|
8
6
|
import "./engine-components/CameraUtils"
|
@@ -11,7 +9,6 @@
|
|
11
9
|
export * from "./engine-components/api";
|
12
10
|
export * from "./engine-components-experimental/api";
|
13
11
|
|
14
|
-
|
15
12
|
// make accessible for external javascript
|
16
13
|
import { Context } from "./engine/engine_setup";
|
17
14
|
const Needle = { Context: Context };
|
@@ -30,12 +27,7 @@
|
|
30
27
|
import * as Components from "./engine-components/codegen/components";
|
31
28
|
registerGlobal(Components);
|
32
29
|
|
33
|
-
Needle["$meta"] = {
|
34
|
-
version: NEEDLE_ENGINE_VERSION,
|
35
|
-
generator: NEEDLE_ENGINE_GENERATOR
|
36
|
-
};
|
37
30
|
|
38
|
-
|
39
31
|
import { GameObject } from "./engine-components/Component";
|
40
32
|
for (const method of Object.getOwnPropertyNames(GameObject)) {
|
41
33
|
switch (method) {
|
@@ -352,9 +352,11 @@
|
|
352
352
|
|
353
353
|
// Adapted from https://discourse.threejs.org/t/camera-zoom-to-fit-object/936/24
|
354
354
|
// Slower but better implementation that takes bones and exact vertex positions into account: https://github.com/google/model-viewer/blob/04e900c5027de8c5306fe1fe9627707f42811b05/packages/model-viewer/src/three-components/ModelScene.ts#L321
|
355
|
-
|
355
|
+
/** Fits the camera to show the objects provided (defaults to the scene if no objects are passed in) */
|
356
|
+
fitCamera(objects?: Array<Object3D>, fitOffset: number = 1.1) {
|
356
357
|
const camera = this._cameraObject as PerspectiveCamera;
|
357
358
|
const controls = this._controls as ThreeOrbitControls | null;
|
359
|
+
if (!objects?.length) objects = this.context.scene.children;
|
358
360
|
|
359
361
|
if (!camera || !controls) return;
|
360
362
|
|
@@ -366,7 +368,7 @@
|
|
366
368
|
// we would get proper view-dependant fit.
|
367
369
|
// Right now it's independent from where the camera is actually looking from,
|
368
370
|
// and thus we're just getting some maximum that will work for sure.
|
369
|
-
|
371
|
+
|
370
372
|
box.makeEmpty();
|
371
373
|
for (const object of objects) {
|
372
374
|
// ignore Box3Helpers
|
@@ -423,7 +425,7 @@
|
|
423
425
|
this._haveAttachedKeyboardEvents = true;
|
424
426
|
document.body.addEventListener("keydown", (e) => {
|
425
427
|
if (e.code === "KeyF") {
|
426
|
-
this.
|
428
|
+
this.fitCamera(objects);
|
427
429
|
}
|
428
430
|
});
|
429
431
|
}
|
@@ -1,24 +1,25 @@
|
|
1
1
|
import { AssetReference } from "../engine/engine_addressables";
|
2
2
|
import { InputEvents } from "../engine/engine_input";
|
3
3
|
import { isLocalNetwork } from "../engine/engine_networking_utils";
|
4
|
-
import {
|
5
|
-
import { getParam, isMobileDevice, setParamWithoutReload } from "../engine/engine_utils";
|
4
|
+
import { getParam, setParamWithoutReload } from "../engine/engine_utils";
|
6
5
|
import { serializable } from "../engine/engine_serialization";
|
7
6
|
import { Behaviour, GameObject } from "./Component";
|
7
|
+
import { registerObservableAttribute } from "../engine/engine_element_extras";
|
8
8
|
|
9
9
|
const debug = getParam("debugsceneswitcher");
|
10
10
|
|
11
11
|
const ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME = "scene";
|
12
|
+
registerObservableAttribute(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME);
|
12
13
|
|
13
|
-
ContextRegistry.registerCallback(ContextEvent.ContextRegistered, async _ => {
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
});
|
14
|
+
// ContextRegistry.registerCallback(ContextEvent.ContextRegistered, async _ => {
|
15
|
+
// // We need to defer import to not get issues with circular dependencies
|
16
|
+
// import("../engine/engine_element").then(res => {
|
17
|
+
// const webcomponent = res.NeedleEngineHTMLElement;
|
18
|
+
// if (debug) console.log("SceneSwitcher: registering scene attribute", webcomponent.observedAttributes);
|
19
|
+
// if (!webcomponent.observedAttributes.includes(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME))
|
20
|
+
// webcomponent.observedAttributes.push(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME);
|
21
|
+
// });
|
22
|
+
// });
|
22
23
|
|
23
24
|
const couldNotLoadScenePromise = Promise.resolve(false);
|
24
25
|
|
@@ -2,40 +2,82 @@
|
|
2
2
|
import { Behaviour, GameObject } from "./Component";
|
3
3
|
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
|
4
4
|
import { EXRLoader } from "three/examples/jsm/loaders/EXRLoader";
|
5
|
-
import { EquirectangularRefractionMapping, sRGBEncoding, Texture, TextureLoader } from "three"
|
5
|
+
import { EquirectangularRefractionMapping, NeverDepth, sRGBEncoding, Texture, TextureLoader } from "three"
|
6
6
|
import { syncField } from "../engine/engine_networking_auto";
|
7
7
|
import { Camera } from "./Camera";
|
8
|
-
import { getParam } from "../engine/engine_utils";
|
8
|
+
import { addAttributeChangeCallback, getParam, removeAttributeChangeCallback } from "../engine/engine_utils";
|
9
9
|
import { ContextRegistry } from "../engine/engine_context_registry";
|
10
|
+
import { registerObservableAttribute } from "../engine/engine_element_extras";
|
11
|
+
import { type IContext } from "../engine/engine_types";
|
10
12
|
|
11
13
|
const debug = getParam("debugskybox");
|
12
14
|
|
15
|
+
registerObservableAttribute("skybox-image");
|
16
|
+
registerObservableAttribute("environment-image");
|
17
|
+
|
18
|
+
function createRemoteSkyboxComponent(context: IContext, url: string, skybox: boolean, environment: boolean, attribute: "skybox-image" | "environment-image") {
|
19
|
+
const remote = new RemoteSkybox();
|
20
|
+
remote.url = url;
|
21
|
+
remote.allowDrop = false;
|
22
|
+
remote.allowNetworking = false;
|
23
|
+
remote.background = skybox;
|
24
|
+
remote.environment = environment;
|
25
|
+
GameObject.addComponent(context.scene, remote);
|
26
|
+
const urlChanged = newValue => {
|
27
|
+
if (typeof newValue !== "string") return;
|
28
|
+
if (debug) console.log(attribute, "CHANGED TO", newValue)
|
29
|
+
remote.setSkybox(newValue);
|
30
|
+
};
|
31
|
+
addAttributeChangeCallback(context.domElement, attribute, urlChanged);
|
32
|
+
remote.addEventListener("destroy", () => {
|
33
|
+
if (debug) console.log("Destroyed attribute remote skybox", attribute);
|
34
|
+
removeAttributeChangeCallback(context.domElement, attribute, urlChanged);
|
35
|
+
});
|
36
|
+
}
|
37
|
+
|
13
38
|
ContextRegistry.addContextCreatedCallback((args) => {
|
14
39
|
const context = args.context;
|
15
40
|
const skyboxImage = context.domElement.getAttribute("skybox-image");
|
16
41
|
const environmentImage = context.domElement.getAttribute("environment-image");
|
42
|
+
|
17
43
|
if (skyboxImage) {
|
18
44
|
if (debug) console.log("Creating remote skybox to load " + skyboxImage);
|
19
|
-
|
20
|
-
remote.url = skyboxImage;
|
21
|
-
remote.allowDrop = false;
|
22
|
-
remote.environment = false;
|
23
|
-
remote.background = true;
|
24
|
-
GameObject.addComponent(context.scene, remote);
|
45
|
+
createRemoteSkyboxComponent(context, skyboxImage, true, false, "skybox-image");
|
25
46
|
}
|
26
47
|
if (environmentImage) {
|
27
|
-
|
28
|
-
|
29
|
-
remote.allowDrop = false;
|
30
|
-
remote.environment = true;
|
31
|
-
remote.background = false;
|
32
|
-
GameObject.addComponent(context.scene, remote);
|
48
|
+
if (debug) console.log("Creating remote environment to load " + environmentImage);
|
49
|
+
createRemoteSkyboxComponent(context, environmentImage, false, true, "environment-image");
|
33
50
|
}
|
34
51
|
});
|
35
52
|
|
53
|
+
|
54
|
+
function ensureGlobalCache() {
|
55
|
+
if (!globalThis["NEEDLE_ENGINE_SKYBOX_TEXTURES"])
|
56
|
+
globalThis["NEEDLE_ENGINE_SKYBOX_TEXTURES"] = new Array<{ src: string, texture: Promise<Texture> }>();
|
57
|
+
return globalThis["NEEDLE_ENGINE_SKYBOX_TEXTURES"];
|
58
|
+
}
|
59
|
+
|
60
|
+
function tryGetPreviouslyLoadedTexture(src: string) {
|
61
|
+
const cache = ensureGlobalCache();
|
62
|
+
const found = cache.find(x => x.src === src);
|
63
|
+
if (found) {
|
64
|
+
if (debug) console.log("Skybox: Found previously loaded texture for " + src);
|
65
|
+
return found.texture;
|
66
|
+
}
|
67
|
+
return null;
|
68
|
+
}
|
69
|
+
function registerLoadedTexture(src: string, texture: Promise<Texture>) {
|
70
|
+
const cache = ensureGlobalCache();
|
71
|
+
// Make sure the cache doesnt get too big
|
72
|
+
while (cache.length > 5) {
|
73
|
+
cache.shift();
|
74
|
+
}
|
75
|
+
cache.push({ src, texture });
|
76
|
+
}
|
77
|
+
|
36
78
|
export class RemoteSkybox extends Behaviour {
|
37
79
|
|
38
|
-
@syncField(
|
80
|
+
@syncField(RemoteSkybox.prototype.urlChangedSyncField)
|
39
81
|
@serializable(URL)
|
40
82
|
url?: string;
|
41
83
|
|
@@ -48,6 +90,9 @@
|
|
48
90
|
@serializable()
|
49
91
|
environment: boolean = true;
|
50
92
|
|
93
|
+
/* set to false if you do not want to apply url change events via networking */
|
94
|
+
allowNetworking: boolean = true;
|
95
|
+
|
51
96
|
private _loader?: RGBELoader | EXRLoader | TextureLoader;
|
52
97
|
private _prevUrl?: string;
|
53
98
|
private _prevLoadedEnvironment?: Texture;
|
@@ -71,7 +116,13 @@
|
|
71
116
|
this.context.mainCameraComponent?.applyClearFlags();
|
72
117
|
}
|
73
118
|
|
119
|
+
private urlChangedSyncField() {
|
120
|
+
if (this.allowNetworking)
|
121
|
+
this.setSkybox(this.url);
|
122
|
+
}
|
123
|
+
|
74
124
|
async setSkybox(url: string | undefined | null) {
|
125
|
+
if (!this.activeAndEnabled) return;
|
75
126
|
if (!url) return;
|
76
127
|
if (!url?.endsWith(".hdr") && !url.endsWith(".exr") && !url.endsWith(".jpg") && !url.endsWith(".png") && !url.endsWith(".jpeg")) {
|
77
128
|
console.warn("Potentially invalid skybox url", this.url, "on", this.name);
|
@@ -89,7 +140,24 @@
|
|
89
140
|
}
|
90
141
|
this._prevUrl = url;
|
91
142
|
|
143
|
+
const envMap = await this.loadTexture(url);
|
144
|
+
if (!envMap) return;
|
145
|
+
// Check if we're still enabled
|
146
|
+
if (!this.enabled) return;
|
147
|
+
// Update the current url
|
148
|
+
this.url = url;
|
149
|
+
const nameIndex = url.lastIndexOf("/");
|
150
|
+
envMap.name = url.substring(nameIndex >= 0 ? nameIndex + 1 : 0);
|
151
|
+
if (this._loader instanceof TextureLoader) {
|
152
|
+
envMap.encoding = sRGBEncoding;
|
153
|
+
}
|
154
|
+
this._prevLoadedEnvironment = envMap;
|
155
|
+
this.applySkybox();
|
156
|
+
}
|
92
157
|
|
158
|
+
private async loadTexture(url: string) {
|
159
|
+
const cached = tryGetPreviouslyLoadedTexture(url);
|
160
|
+
if (cached) return await cached;
|
93
161
|
const isEXR = url.endsWith(".exr");
|
94
162
|
const isHdr = url.endsWith(".hdr");
|
95
163
|
if (isEXR) {
|
@@ -106,19 +174,10 @@
|
|
106
174
|
}
|
107
175
|
|
108
176
|
if (debug) console.log("Loading skybox: " + url);
|
109
|
-
const
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
// Update the current url
|
114
|
-
this.url = url;
|
115
|
-
const nameIndex = url.lastIndexOf("/");
|
116
|
-
envMap.name = url.substring(nameIndex >= 0 ? nameIndex + 1 : 0);
|
117
|
-
if (this._loader instanceof TextureLoader) {
|
118
|
-
envMap.encoding = sRGBEncoding;
|
119
|
-
}
|
120
|
-
this._prevLoadedEnvironment = envMap;
|
121
|
-
this.applySkybox();
|
177
|
+
const loadingTask = this._loader.loadAsync(url);
|
178
|
+
registerLoadedTexture(url, loadingTask);
|
179
|
+
const envMap = await loadingTask;
|
180
|
+
return envMap;
|
122
181
|
}
|
123
182
|
|
124
183
|
private applySkybox() {
|
@@ -0,0 +1,11 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
/*
|
4
|
+
DO NOT IMPORT ENGINE_ELEMENT FROM HERE
|
5
|
+
*/
|
6
|
+
|
7
|
+
export async function registerObservableAttribute(name: string) {
|
8
|
+
const { NeedleEngineHTMLElement } = await import("./engine_element");
|
9
|
+
if (!NeedleEngineHTMLElement.observedAttributes.includes(name))
|
10
|
+
NeedleEngineHTMLElement.observedAttributes.push(name);
|
11
|
+
}
|