@@ -1,7 +1,8 @@
|
|
1
1
|
import { FileLoader } from "three";
|
2
2
|
|
3
|
-
|
3
|
+
const loader = new FileLoader();
|
4
4
|
|
5
|
+
/** @internal */
|
5
6
|
export async function loadFileAsync(url) {
|
6
7
|
return new Promise((resolve, reject) => {
|
7
8
|
loader.load(url, resolve, undefined, reject);
|
@@ -21,6 +21,9 @@
|
|
21
21
|
|
22
22
|
class Vec2 { x!: number; y!: number }
|
23
23
|
|
24
|
+
/**
|
25
|
+
* Animation component to play animations on a GameObject
|
26
|
+
*/
|
24
27
|
export class Animation extends Behaviour {
|
25
28
|
|
26
29
|
@serializable()
|
@@ -1,7 +1,9 @@
|
|
1
|
-
import { Mathf } from "../engine/engine_math.js";
|
2
1
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
3
2
|
|
4
|
-
|
3
|
+
/**
|
4
|
+
* Keyframe is a representation of a keyframe in an AnimationCurve.
|
5
|
+
*/
|
6
|
+
export class Keyframe {
|
5
7
|
@serializable()
|
6
8
|
time!: number;
|
7
9
|
@serializable()
|
@@ -8,6 +8,7 @@
|
|
8
8
|
import { Behaviour, GameObject } from "../Component.js";
|
9
9
|
import { AvatarMarker } from "../webxr/WebXRAvatar.js";
|
10
10
|
|
11
|
+
/** @internal */
|
11
12
|
export class Avatar_POI {
|
12
13
|
|
13
14
|
public static Pois: { obj: Object3D, avatar: AvatarMarker | null }[] = [];
|
@@ -46,6 +47,7 @@
|
|
46
47
|
public position: Vector3 = new Vector3();
|
47
48
|
}
|
48
49
|
|
50
|
+
/** @internal */
|
49
51
|
export class Avatar_Brain_LookAt extends Behaviour {
|
50
52
|
|
51
53
|
public set controlledTarget(target: Object3D) {
|
@@ -8,6 +8,7 @@
|
|
8
8
|
|
9
9
|
const debug = utils.getParam("debugmouth");
|
10
10
|
|
11
|
+
/** @internal */
|
11
12
|
export class Avatar_MouthShapes extends Behaviour {
|
12
13
|
@serializable(Object3D)
|
13
14
|
public idle: Object3D[] = [];
|
@@ -4,6 +4,7 @@
|
|
4
4
|
import { Voip } from "../Voip.js";
|
5
5
|
import { AvatarMarker } from "../webxr/WebXRAvatar.js";
|
6
6
|
|
7
|
+
/** @internal */
|
7
8
|
export class Avatar_MustacheShake extends Behaviour {
|
8
9
|
private voip: Voip | null = null;
|
9
10
|
private marker: AvatarMarker | null = null;
|
@@ -5,6 +5,7 @@
|
|
5
5
|
import { XRFlag } from "../webxr/XRFlag.js";
|
6
6
|
|
7
7
|
|
8
|
+
/** @internal */
|
8
9
|
export class AvatarBlink_Simple extends Behaviour {
|
9
10
|
|
10
11
|
@serializable(Object3D)
|
@@ -5,6 +5,7 @@
|
|
5
5
|
import { Behaviour, GameObject } from "../Component.js";
|
6
6
|
import { Avatar_Brain_LookAt } from "./Avatar_Brain_LookAt.js";
|
7
7
|
|
8
|
+
/** @internal */
|
8
9
|
export class AvatarEyeLook_Rotation extends Behaviour {
|
9
10
|
|
10
11
|
@serializable(Object3D)
|
@@ -4,6 +4,9 @@
|
|
4
4
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
5
|
import { Behaviour } from "./Component.js";
|
6
6
|
|
7
|
+
/**
|
8
|
+
* AxesHelper is a component that displays the axes of the object in the scene.
|
9
|
+
*/
|
7
10
|
export class AxesHelper extends Behaviour {
|
8
11
|
@serializable()
|
9
12
|
public length: number = 1;
|
@@ -1,4 +1,6 @@
|
|
1
1
|
import { IContext } from "../engine_types.js";
|
2
|
+
import { generateQRCode } from "../engine_utils.js";
|
3
|
+
import { onXRSessionEnd, onXRSessionStart } from "../xr/events.js";
|
2
4
|
import { getIconElement } from "./icons.js";
|
3
5
|
|
4
6
|
/** Use the ButtonsFactory to create buttons with icons and functionality
|
@@ -8,7 +10,10 @@
|
|
8
10
|
export class ButtonsFactory {
|
9
11
|
|
10
12
|
private static _instance?: ButtonsFactory;
|
11
|
-
/**
|
13
|
+
/** Get access to the default HTML button factory.
|
14
|
+
* Use this to get or create default Needle Engine buttons that can be added to your HTML UI
|
15
|
+
* If you want to create a new factory and create new button instances instead of shared buttons, use `ButtonsFactory.create()` instead
|
16
|
+
*/
|
12
17
|
static getOrCreate() {
|
13
18
|
if (!this._instance) {
|
14
19
|
this._instance = new ButtonsFactory();
|
@@ -104,4 +109,115 @@
|
|
104
109
|
};
|
105
110
|
return button;
|
106
111
|
}
|
112
|
+
|
113
|
+
|
114
|
+
private _qrButton?: HTMLButtonElement;
|
115
|
+
/** Create a QR code button (or return the existing one if it already exists)
|
116
|
+
* The QR code button will show a QR code that can be scanned to open the current page on a phone
|
117
|
+
* The QR code will be generated with the current URL when the button is clicked
|
118
|
+
* @returns the QR code button element
|
119
|
+
*/
|
120
|
+
createQRCode(): HTMLButtonElement {
|
121
|
+
|
122
|
+
if (this._qrButton) return this._qrButton;
|
123
|
+
|
124
|
+
const qrCodeButton = document.createElement("button");
|
125
|
+
this._qrButton = qrCodeButton;
|
126
|
+
qrCodeButton.innerText = "QR Code";
|
127
|
+
qrCodeButton.prepend(getIconElement("qr_code"));
|
128
|
+
qrCodeButton.title = "Scan this QR code with your phone to open this page";
|
129
|
+
this.hideElementDuringXRSession(qrCodeButton);
|
130
|
+
|
131
|
+
|
132
|
+
const qrCodeContainer = document.createElement("div");
|
133
|
+
qrCodeContainer.style.position = "fixed";
|
134
|
+
qrCodeContainer.style.display = "inline-block";
|
135
|
+
qrCodeContainer.style.padding = "1rem";
|
136
|
+
qrCodeContainer.style.backgroundColor = "white";
|
137
|
+
qrCodeContainer.style.borderRadius = "0.4rem";
|
138
|
+
qrCodeContainer.style.cursor = "pointer";
|
139
|
+
qrCodeContainer.style.zIndex = "1000";
|
140
|
+
qrCodeContainer.style.boxShadow = "0 0 12px rgba(0, 0, 0, 0.2)";
|
141
|
+
|
142
|
+
const qrCodeElement = document.createElement("div");
|
143
|
+
qrCodeElement.classList.add("qr-code-container");
|
144
|
+
qrCodeContainer.appendChild(qrCodeElement);
|
145
|
+
|
146
|
+
qrCodeButton.addEventListener("click", () => {
|
147
|
+
if (qrCodeContainer.parentNode) return hideQRCode();
|
148
|
+
showQRCode();
|
149
|
+
});
|
150
|
+
|
151
|
+
/** shows the QRCode near the button */
|
152
|
+
async function showQRCode() {
|
153
|
+
// generate the qr code when the button is clicked
|
154
|
+
// this ensures that we get the QRcode with the latest URL
|
155
|
+
await generateAndInsertQRCode();
|
156
|
+
// TODO: make sure it doesnt overflow the screen
|
157
|
+
// we need to add the qrCodeContainer to the body to get the correct size
|
158
|
+
document.body.appendChild(qrCodeContainer);
|
159
|
+
const containerRect = qrCodeElement.getBoundingClientRect();
|
160
|
+
const buttonRect = qrCodeButton.getBoundingClientRect();
|
161
|
+
qrCodeContainer.style.left = (buttonRect.left + buttonRect.width * .5 - containerRect.width * .5) + "px";
|
162
|
+
const isButtonInTopHalf = buttonRect.top < containerRect.height;
|
163
|
+
if (isButtonInTopHalf)
|
164
|
+
qrCodeContainer.style.top = `calc(${buttonRect.bottom}px + ${qrCodeContainer.style.padding} * .6)`;
|
165
|
+
else
|
166
|
+
qrCodeContainer.style.top = `calc(${buttonRect.top - containerRect.height}px - ${qrCodeContainer.style.padding} * 2.5)`;
|
167
|
+
qrCodeContainer.style.opacity = "0";
|
168
|
+
qrCodeContainer.style.pointerEvents = "all";
|
169
|
+
qrCodeContainer.style.transition = "opacity 0.2s ease-in-out";
|
170
|
+
|
171
|
+
// context click to hide the QR code again, if we dont timeout the event will be triggered immediately
|
172
|
+
setTimeout(() => {
|
173
|
+
qrCodeContainer.style.opacity = "1";
|
174
|
+
window.addEventListener("click", hideQRCode, { once: true })
|
175
|
+
});
|
176
|
+
window.addEventListener("resize", hideQRCode);
|
177
|
+
window.addEventListener("scroll", hideQRCode);
|
178
|
+
|
179
|
+
document.body.appendChild(qrCodeContainer);
|
180
|
+
}
|
181
|
+
|
182
|
+
/** hides to QRCode overlay and unsubscribes from events */
|
183
|
+
function hideQRCode() {
|
184
|
+
qrCodeContainer.style.pointerEvents = "none";
|
185
|
+
qrCodeContainer.style.transition = "opacity 0.2s";
|
186
|
+
qrCodeContainer.style.opacity = "0";
|
187
|
+
setTimeout(() => qrCodeContainer.parentNode?.removeChild(qrCodeContainer), 500);
|
188
|
+
window.removeEventListener("click", hideQRCode);
|
189
|
+
window.removeEventListener("resize", hideQRCode);
|
190
|
+
window.removeEventListener("scroll", hideQRCode);
|
191
|
+
};
|
192
|
+
|
193
|
+
/** generates a QR code and inserts it into the qrCodeElement */
|
194
|
+
async function generateAndInsertQRCode() {
|
195
|
+
const size = 200;
|
196
|
+
const code = await generateQRCode({
|
197
|
+
text: window.location.href,
|
198
|
+
width: size,
|
199
|
+
height: size,
|
200
|
+
});
|
201
|
+
qrCodeElement.innerHTML = "";
|
202
|
+
qrCodeElement.appendChild(code);
|
203
|
+
}
|
204
|
+
|
205
|
+
// lazily create the qr button
|
206
|
+
qrCodeButton.addEventListener("pointerenter", () => {
|
207
|
+
generateAndInsertQRCode();
|
208
|
+
}, { once: true });
|
209
|
+
|
210
|
+
return qrCodeButton;
|
211
|
+
}
|
212
|
+
|
213
|
+
private hideElementDuringXRSession(element: HTMLElement) {
|
214
|
+
onXRSessionStart(_ => {
|
215
|
+
element["previous-display"] = element.style.display;
|
216
|
+
element.style.display = "none";
|
217
|
+
});
|
218
|
+
onXRSessionEnd(_ => {
|
219
|
+
if (element["previous-display"] != undefined)
|
220
|
+
element.style.display = element["previous-display"];
|
221
|
+
});
|
222
|
+
}
|
107
223
|
}
|
@@ -21,6 +21,13 @@
|
|
21
21
|
// onDeserialize?(key: string, value: any): any | void;
|
22
22
|
// }
|
23
23
|
|
24
|
+
/**
|
25
|
+
* All {@type Object3D} types that are loaded in Needle Engine do automatically receive the GameObject extensions like `addComponent` etc.
|
26
|
+
* Many of the GameObject methods can be imported directly via `@needle-tools/engine` as well:
|
27
|
+
* ```typescript
|
28
|
+
* import { addComponent } from "@needle-tools/engine";
|
29
|
+
* ```
|
30
|
+
*/
|
24
31
|
export abstract class GameObject extends Object3D implements Object3D, IGameObject {
|
25
32
|
|
26
33
|
// these are implemented via threejs object extensions
|
@@ -303,17 +310,33 @@
|
|
303
310
|
|
304
311
|
|
305
312
|
|
306
|
-
/**
|
313
|
+
/**
|
314
|
+
* Needle Engine component base class. Component's are the main building blocks of the Needle Engine.
|
315
|
+
* Derive from {@link Behaviour} to implement your own using the provided lifecycle methods.
|
316
|
+
* Components can be added to threejs objects using `{@link addComponent}` or `{@link GameObject.addComponent}`
|
307
317
|
*
|
308
318
|
* The most common lifecycle methods are `awake`, `start`, `onEnable`, `onDisable` `update` and `onDestroy`.
|
309
319
|
* XR specific callbacks include `onEnterXR`, `onLeaveXR`, `onUpdateXR`, `onControllerAdded` and `onControllerRemoved`.
|
310
320
|
* To receive pointer events implement `onPointerDown`, `onPointerUp`, `onPointerEnter`, `onPointerExit` and `onPointerMove`.
|
321
|
+
*
|
322
|
+
* @example
|
323
|
+
* ```typescript
|
324
|
+
* import { Behaviour } from "@engine/engine/engine";
|
325
|
+
* export class MyComponent extends Behaviour {
|
326
|
+
* start() {
|
327
|
+
* console.log("Hello World");
|
328
|
+
* }
|
329
|
+
* update() {
|
330
|
+
* console.log("Frame", this.context.time.frame);
|
331
|
+
* }
|
332
|
+
* }
|
333
|
+
* ```
|
311
334
|
*/
|
312
335
|
export abstract class Component implements IComponent, EventTarget,
|
313
336
|
Partial<INeedleXRSessionEventReceiver>,
|
314
337
|
Partial<IPointerEventHandler>
|
315
338
|
{
|
316
|
-
|
339
|
+
/** @internal */
|
317
340
|
get isComponent(): boolean { return true; }
|
318
341
|
|
319
342
|
private __context: Context | undefined;
|
@@ -324,13 +347,16 @@
|
|
324
347
|
set context(context: Context) {
|
325
348
|
this.__context = context;
|
326
349
|
}
|
327
|
-
/** shorthand for `this.context.scene`
|
350
|
+
/** shorthand for `this.context.scene`
|
351
|
+
* @returns the scene of the context */
|
328
352
|
get scene(): Scene { return this.context.scene; }
|
329
353
|
|
354
|
+
/** @returns the layer of the gameObject this component is attached to */
|
330
355
|
get layer(): number {
|
331
356
|
return this.gameObject?.userData?.layer;
|
332
357
|
}
|
333
358
|
|
359
|
+
/** @returns the name of the gameObject this component is attached to */
|
334
360
|
get name(): string {
|
335
361
|
if (this.gameObject?.name) {
|
336
362
|
return this.gameObject.name;
|
@@ -348,6 +374,7 @@
|
|
348
374
|
this.__name = str;
|
349
375
|
}
|
350
376
|
}
|
377
|
+
/** @returns the tag of the gameObject this component is attached to */
|
351
378
|
get tag(): string {
|
352
379
|
return this.gameObject?.userData.tag;
|
353
380
|
}
|
@@ -357,6 +384,7 @@
|
|
357
384
|
this.gameObject.userData.tag = str;
|
358
385
|
}
|
359
386
|
}
|
387
|
+
/** Is the gameObject marked as static */
|
360
388
|
get static() {
|
361
389
|
return this.gameObject?.userData.static;
|
362
390
|
}
|
@@ -90,6 +90,7 @@
|
|
90
90
|
export { InstanceHandle } from "../RendererInstancing.js";
|
91
91
|
export { InstancingHandler } from "../RendererInstancing.js";
|
92
92
|
export { Interactable } from "../Interactable.js";
|
93
|
+
export { Keyframe } from "../AnimationCurve.js";
|
93
94
|
export { Light } from "../Light.js";
|
94
95
|
export { LimitVelocityOverLifetimeModule } from "../ParticleSystemModules.js";
|
95
96
|
export { LODGroup } from "../LODGroup.js";
|
@@ -16,6 +16,9 @@
|
|
16
16
|
// - backface shadowing (slightly less than front faces)
|
17
17
|
// - node can simply be scaled in Y to adjust max. ground height
|
18
18
|
|
19
|
+
/**
|
20
|
+
* ContactShadows is a component that allows to display contact shadows in the scene.
|
21
|
+
*/
|
19
22
|
export class ContactShadows extends Behaviour {
|
20
23
|
|
21
24
|
@serializable()
|
@@ -43,7 +46,8 @@
|
|
43
46
|
private horizontalBlurMaterial?: ShaderMaterial;
|
44
47
|
private verticalBlurMaterial?: ShaderMaterial;
|
45
48
|
|
46
|
-
|
49
|
+
/** @internal */
|
50
|
+
start(): void {
|
47
51
|
if(debug) console.log("Create ContactShadows on " + this.gameObject.name, this)
|
48
52
|
const textureSize = 512;
|
49
53
|
|
@@ -148,6 +152,7 @@
|
|
148
152
|
this.shadowGroup.visible = false;
|
149
153
|
}
|
150
154
|
|
155
|
+
/** @internal */
|
151
156
|
onDestroy(): void {
|
152
157
|
// dispose the render targets
|
153
158
|
this.renderTarget?.dispose();
|
@@ -164,6 +169,7 @@
|
|
164
169
|
this.occluderMesh?.geometry.dispose();
|
165
170
|
}
|
166
171
|
|
172
|
+
/** @internal */
|
167
173
|
onBeforeRender(_frame: XRFrame | null): void {
|
168
174
|
const scene = this.context.scene;
|
169
175
|
const renderer = this.context.renderer;
|
@@ -12,16 +12,22 @@
|
|
12
12
|
|
13
13
|
const debug = getParam("debugaddressables");
|
14
14
|
|
15
|
+
/**
|
16
|
+
* The Addressables class is used to register and manage {@link AssetReference} types
|
17
|
+
* It can be accessed via {@link Context.Current} or `{@link Context.addressables}` (e.g. `this.context.addressables` in a component)
|
18
|
+
*/
|
15
19
|
export class Addressables {
|
16
20
|
|
17
21
|
private _context: Context;
|
18
22
|
private _assetReferences: { [key: string]: AssetReference } = {};
|
19
23
|
|
24
|
+
/** @internal */
|
20
25
|
constructor(context: Context) {
|
21
26
|
this._context = context;
|
22
27
|
this._context.pre_update_callbacks.push(this.preUpdate);
|
23
28
|
}
|
24
29
|
|
30
|
+
/** @internal */
|
25
31
|
dispose() {
|
26
32
|
const preUpdateIndex = this._context.pre_update_callbacks.indexOf(this.preUpdate);
|
27
33
|
if (preUpdateIndex >= 0) {
|
@@ -33,7 +39,7 @@
|
|
33
39
|
}
|
34
40
|
this._assetReferences = {};
|
35
41
|
}
|
36
|
-
|
42
|
+
|
37
43
|
private preUpdate = () => {
|
38
44
|
|
39
45
|
}
|
@@ -43,6 +49,7 @@
|
|
43
49
|
return this._assetReferences[key] || null;
|
44
50
|
}
|
45
51
|
|
52
|
+
/** @internal */
|
46
53
|
registerAssetReference(ref: AssetReference): AssetReference {
|
47
54
|
if (!ref.uri) return ref;
|
48
55
|
if (!this._assetReferences[ref.uri]) {
|
@@ -55,6 +62,7 @@
|
|
55
62
|
return ref;
|
56
63
|
}
|
57
64
|
|
65
|
+
/** @internal */
|
58
66
|
unregisterAssetReference(ref: AssetReference) {
|
59
67
|
if (!ref.uri) return;
|
60
68
|
delete this._assetReferences[ref.uri];
|
@@ -104,6 +112,7 @@
|
|
104
112
|
|
105
113
|
private static currentlyInstantiating: Map<string, number> = new Map<string, number>();
|
106
114
|
|
115
|
+
/** The loaded asset */
|
107
116
|
get asset(): any {
|
108
117
|
return this._glbRoot ?? this._asset;
|
109
118
|
}
|
@@ -115,10 +124,14 @@
|
|
115
124
|
private _loading?: PromiseLike<any>;
|
116
125
|
|
117
126
|
// TODO: rename to url
|
127
|
+
/** The url of the loaded asset (or the asset to be loaded) */
|
118
128
|
get uri(): string {
|
119
129
|
return this._url;
|
120
130
|
}
|
121
131
|
|
132
|
+
/**
|
133
|
+
* This is the loaded asset root object. If the asset is a glb/gltf file this will be the {@link three#Scene} object.
|
134
|
+
*/
|
122
135
|
get rawAsset(): any { return this._asset; }
|
123
136
|
|
124
137
|
private _asset: any;
|
@@ -132,6 +145,7 @@
|
|
132
145
|
private _isLoadingRawBinary: boolean = false;
|
133
146
|
private _rawBinary?: ArrayBuffer | null;
|
134
147
|
|
148
|
+
/** @internal */
|
135
149
|
constructor(uri: string, hash?: string, asset: any = null) {
|
136
150
|
this._url = uri;
|
137
151
|
this._hash = hash;
|
@@ -157,7 +171,8 @@
|
|
157
171
|
return !this.asset || this.asset.__destroyed === true || isDestroyed(this.asset) === true;
|
158
172
|
}
|
159
173
|
|
160
|
-
/**
|
174
|
+
/**
|
175
|
+
* @returns `true` if the asset has been loaded (via preload) or if it exists already (assigned to `asset`) */
|
161
176
|
isLoaded() { return this._rawBinary || this.asset !== undefined }
|
162
177
|
|
163
178
|
/** frees previously allocated memory and destroys the current `asset` instance (if any) */
|
@@ -27,6 +27,9 @@
|
|
27
27
|
onUserInteraction();
|
28
28
|
})
|
29
29
|
|
30
|
+
/**
|
31
|
+
* The Application class can be used to mute audio globally, and to check if the application (canvas) is currently visible (it's tab is active and not minimized).
|
32
|
+
*/
|
30
33
|
export class Application extends EventTarget {
|
31
34
|
|
32
35
|
public static get userInteractionRegistered(): boolean {
|
@@ -56,16 +59,21 @@
|
|
56
59
|
|
57
60
|
private context: Context;
|
58
61
|
|
62
|
+
/** @returns true if the document is focused */
|
59
63
|
public get hasFocus(): boolean {
|
60
64
|
return document.hasFocus();
|
61
65
|
}
|
62
66
|
|
67
|
+
/**
|
68
|
+
* @returns true if the application is currently visible (it's tab is active and not minimized)
|
69
|
+
*/
|
63
70
|
public get isVisible(): boolean {
|
64
71
|
return this._isVisible;
|
65
72
|
}
|
66
73
|
|
67
74
|
private _isVisible: boolean = true;
|
68
75
|
|
76
|
+
/** @internal */
|
69
77
|
constructor(context: Context) {
|
70
78
|
super();
|
71
79
|
this.context = context;
|
@@ -2,7 +2,9 @@
|
|
2
2
|
|
3
3
|
import { Application } from "./engine_application.js";
|
4
4
|
|
5
|
-
/**
|
5
|
+
/**
|
6
|
+
* @internal
|
7
|
+
* Ensure the audio context is resumed if it gets suspended or interrupted */
|
6
8
|
export function ensureAudioContextIsResumed() {
|
7
9
|
Application.registerWaitForAllowAudio(() => {
|
8
10
|
// this is a fix for https://github.com/mrdoob/three.js/issues/27779 & https://linear.app/needle/issue/NE-4257
|
@@ -5,10 +5,13 @@
|
|
5
5
|
|
6
6
|
const $cameraController = Symbol("cameraController");
|
7
7
|
|
8
|
+
/** Get the camera controller for the given camera (if any)
|
9
|
+
*/
|
8
10
|
export function getCameraController(cam: Camera): ICameraController | null {
|
9
11
|
return cam[$cameraController];
|
10
12
|
}
|
11
13
|
|
14
|
+
/** Set the camera controller for the given camera */
|
12
15
|
export function setCameraController(cam: Camera, cameraController: ICameraController, active: boolean) {
|
13
16
|
if (active)
|
14
17
|
cam[$cameraController] = cameraController;
|
@@ -21,6 +24,7 @@
|
|
21
24
|
|
22
25
|
const $autofit = Symbol("camera autofit");
|
23
26
|
|
27
|
+
/** @internal */
|
24
28
|
export function useForAutoFit(obj: Object3D): boolean {
|
25
29
|
// if autofit is not defined we assume it may be included
|
26
30
|
if (obj[$autofit] === undefined) return true;
|
@@ -28,6 +32,7 @@
|
|
28
32
|
return obj[$autofit] !== false;
|
29
33
|
}
|
30
34
|
|
35
|
+
/** @internal */
|
31
36
|
export function setAutoFitEnabled(obj: Object3D, enabled: boolean): void {
|
32
37
|
obj[$autofit] = enabled;
|
33
38
|
|
@@ -19,9 +19,11 @@
|
|
19
19
|
tryEval(`globalThis["__NEEDLE_ENGINE_GENERATOR__"] = "` + NEEDLE_ENGINE_GENERATOR + `";`)
|
20
20
|
tryEval(`globalThis["__NEEDLE_PROJECT_BUILD_TIME__"] = "` + NEEDLE_PROJECT_BUILD_TIME + `";`)
|
21
21
|
|
22
|
-
|
22
|
+
/** The version of the Needle engine */
|
23
23
|
export const VERSION = NEEDLE_ENGINE_VERSION;
|
24
|
+
/** The generator used to export the scene / build the web project */
|
24
25
|
export const GENERATOR = NEEDLE_ENGINE_GENERATOR;
|
26
|
+
/** The build time of the project */
|
25
27
|
export const BUILD_TIME = NEEDLE_PROJECT_BUILD_TIME;
|
26
28
|
if (debug) console.log(`Engine version: ${VERSION} (generator: ${GENERATOR})\nProject built at ${BUILD_TIME}`);
|
27
29
|
|
@@ -1,5 +1,7 @@
|
|
1
1
|
import { type IComponent, type IContext, type LoadedGLTF } from "./engine_types.js";
|
2
2
|
|
3
|
+
/** The various events that can be dispatched by a Needle Engine {@link IContext} instance
|
4
|
+
*/
|
3
5
|
export enum ContextEvent {
|
4
6
|
/** called when the context is registered to the registry, the context is not fully initialized at this point */
|
5
7
|
ContextRegistered = "ContextRegistered",
|
@@ -29,6 +31,13 @@
|
|
29
31
|
|
30
32
|
/** Use to register to various Needle Engine context events and to get access to all current instances
|
31
33
|
* e.g. when being created in the DOM
|
34
|
+
* @example
|
35
|
+
* ```typescript
|
36
|
+
* import { NeedleEngine } from "./engine/engine_context_registry.js";
|
37
|
+
* NeedleEngine.addContextCreatedCallback((evt) => {
|
38
|
+
* console.log("Context created", evt.context);
|
39
|
+
* });
|
40
|
+
* ```
|
32
41
|
* */
|
33
42
|
export class ContextRegistry {
|
34
43
|
|
@@ -64,11 +73,15 @@
|
|
64
73
|
|
65
74
|
private static _callbacks: { [evt: string]: Array<ContextCallback> } = {};
|
66
75
|
|
76
|
+
/**
|
77
|
+
* Register a callback to be called when the given event occurs
|
78
|
+
*/
|
67
79
|
static registerCallback(evt: ContextEvent, callback: ContextCallback) {
|
68
80
|
if (!this._callbacks[evt]) this._callbacks[evt] = [];
|
69
81
|
this._callbacks[evt].push(callback);
|
70
82
|
}
|
71
83
|
|
84
|
+
/** Unregister a callback */
|
72
85
|
static unregisterCallback(evt: ContextEvent, callback: ContextCallback) {
|
73
86
|
if (!this._callbacks[evt]) return;
|
74
87
|
const index = this._callbacks[evt].indexOf(callback);
|
@@ -93,9 +106,15 @@
|
|
93
106
|
return Promise.all(promises);
|
94
107
|
}
|
95
108
|
|
109
|
+
/**
|
110
|
+
* Register a callback to be called when a context is created
|
111
|
+
*/
|
96
112
|
static addContextCreatedCallback(callback: ContextCallback) {
|
97
113
|
this.registerCallback(ContextEvent.ContextCreated, callback);
|
98
114
|
}
|
115
|
+
/**
|
116
|
+
* Register a callback to be called when a context is registered
|
117
|
+
*/
|
99
118
|
static addContextDestroyedCallback(callback: ContextCallback) {
|
100
119
|
this.registerCallback(ContextEvent.ContextDestroyed, callback);
|
101
120
|
}
|
@@ -114,6 +114,20 @@
|
|
114
114
|
}
|
115
115
|
}
|
116
116
|
|
117
|
+
/**
|
118
|
+
* The context is the main object that holds all the data and state of the Needle Engine.
|
119
|
+
* It can be used to access the scene, renderer, camera, input, physics, networking, and more.
|
120
|
+
* @example
|
121
|
+
* ```typescript
|
122
|
+
* import { Behaviour } from "@needle-tools/engine";
|
123
|
+
* export class MyScript extends Behaviour {
|
124
|
+
* start() {
|
125
|
+
* console.log("Hello from MyScript");
|
126
|
+
* this.context.scene.add(new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial()));
|
127
|
+
* }
|
128
|
+
* }
|
129
|
+
* ```
|
130
|
+
*/
|
117
131
|
export class Context implements IContext {
|
118
132
|
|
119
133
|
private static _defaultTargetFramerate: { value?: number } = { value: 60 }
|
@@ -131,11 +145,19 @@
|
|
131
145
|
alpha: false,
|
132
146
|
powerPreference: "high-performance",
|
133
147
|
};
|
148
|
+
/** The default parameters that will be used when creating a new WebGLRenderer.
|
149
|
+
* Modify in global context to change the default parameters for all new contexts.
|
150
|
+
* @example
|
151
|
+
* ```typescript
|
152
|
+
* import { Context } from "@needle-tools/engine";
|
153
|
+
* Context.DefaultWebGLRendererParameters.antialias = false;
|
154
|
+
* ```
|
155
|
+
*/
|
134
156
|
static get DefaultWebGLRendererParameters(): WebGLRendererParameters {
|
135
157
|
return Context._defaultWebglRendererParameters;
|
136
158
|
}
|
137
159
|
|
138
|
-
/**
|
160
|
+
/** The needle engine version */
|
139
161
|
get version() {
|
140
162
|
return VERSION;
|
141
163
|
}
|
@@ -150,7 +172,9 @@
|
|
150
172
|
ContextRegistry.Current = context;
|
151
173
|
}
|
152
174
|
|
175
|
+
/** The name of the context */
|
153
176
|
name: string;
|
177
|
+
/** An alias for the context */
|
154
178
|
alias: string | undefined | null;
|
155
179
|
/** When the renderer or camera are managed by an external process (e.g. when running in r3f context).
|
156
180
|
* When this is false you are responsible to call update(timestamp, xframe.
|
@@ -178,7 +202,7 @@
|
|
178
202
|
/** used to append to loaded assets */
|
179
203
|
hash?: string;
|
180
204
|
|
181
|
-
/**
|
205
|
+
/** The `<needle-engine>` web component */
|
182
206
|
domElement: HTMLElement;
|
183
207
|
|
184
208
|
appendHTMLElement(element: HTMLElement) {
|
@@ -310,9 +334,11 @@
|
|
310
334
|
readonly new_scripts_post_setup_callbacks: Function[] = [];
|
311
335
|
readonly new_scripts_xr: INeedleXRSessionEventReceiver[] = [];
|
312
336
|
|
337
|
+
/** The main camera component of the scene - this camera is used for rendering */
|
313
338
|
mainCameraComponent: ICamera | undefined;
|
314
339
|
|
315
|
-
|
340
|
+
|
341
|
+
/** The main camera of the scene - this camera is used for rendering */
|
316
342
|
get mainCamera(): Camera | null {
|
317
343
|
if (this._camera) {
|
318
344
|
return this._camera;
|
@@ -325,9 +351,11 @@
|
|
325
351
|
}
|
326
352
|
return null;
|
327
353
|
}
|
354
|
+
/** Set the main camera of the scene. If set to null the camera of the {@link mainCameraComponent} will be used - this camera is used for rendering */
|
328
355
|
set mainCamera(cam: Camera | null) {
|
329
356
|
this._camera = cam;
|
330
357
|
}
|
358
|
+
private _camera: Camera | null = null;
|
331
359
|
|
332
360
|
application: Application;
|
333
361
|
/** access timings (current frame number, deltaTime, timeScale, ...) */
|
@@ -18,8 +18,16 @@
|
|
18
18
|
scale?: Vec3,
|
19
19
|
}
|
20
20
|
|
21
|
+
/**
|
22
|
+
* Utility class to create primitive objects
|
23
|
+
* @example
|
24
|
+
* ```typescript
|
25
|
+
* const cube = ObjectUtils.createPrimitive("Cube", { name: "Cube", position: { x: 0, y: 0, z: 0 } });
|
26
|
+
* ```
|
27
|
+
*/
|
21
28
|
export class ObjectUtils {
|
22
29
|
|
30
|
+
/** Creates a primitive object like a Cube or Sphere */
|
23
31
|
static createPrimitive(type: PrimitiveType | PrimitiveTypeNames, opts?: ObjectOptions): Mesh {
|
24
32
|
let obj: Mesh;
|
25
33
|
const color = 0xffffff;
|
@@ -1,14 +1,16 @@
|
|
1
1
|
|
2
2
|
|
3
3
|
|
4
|
-
|
4
|
+
/**
|
5
|
+
* @internal
|
6
|
+
*/
|
5
7
|
export declare type EditorModification = {
|
6
8
|
guid: string,
|
7
9
|
propertyName: string,
|
8
10
|
value: any
|
9
11
|
}
|
10
12
|
|
11
|
-
/** Implement to receive callbacks from @needle-tools/editor-sync package */
|
13
|
+
/** Implement to receive callbacks from {@type @needle-tools/editor-sync} package */
|
12
14
|
export interface IEditorModification {
|
13
15
|
/**
|
14
16
|
* Called when a modification is made through the external editor (called from @needle-tools/editor-sync)
|
@@ -21,10 +23,7 @@
|
|
21
23
|
onAfterEditorModification?(mod: EditorModification): void;
|
22
24
|
}
|
23
25
|
|
24
|
-
|
25
|
-
// export function setEditorModificationCache(cache: Map<string, EditorModification>) {
|
26
|
-
// cache = editorCache;
|
27
|
-
// }
|
26
|
+
/** @internal */
|
28
27
|
export function getEditorModificationCache() {
|
29
28
|
return globalThis["NeedleEditorSync.ModificationCache"] as null | undefined | Map<string, EditorModification>;
|
30
29
|
}
|
@@ -53,7 +53,9 @@
|
|
53
53
|
"environment-image"?: string,
|
54
54
|
}
|
55
55
|
|
56
|
-
|
56
|
+
/**
|
57
|
+
* Available attributes for the `<needle-engine>` web component
|
58
|
+
*/
|
57
59
|
export type NeedleEngineAttributes =
|
58
60
|
MainAttributes
|
59
61
|
& Partial<Omit<HTMLElement, "style">>
|
@@ -11,11 +11,13 @@
|
|
11
11
|
|
12
12
|
declare type LoadingStyleOption = "dark" | "light" | "auto";
|
13
13
|
|
14
|
+
/** @internal */
|
14
15
|
export class LoadingElementOptions {
|
15
16
|
className?: string;
|
16
17
|
additionalClasses?: string[];
|
17
18
|
}
|
18
19
|
|
20
|
+
/** @internal */
|
19
21
|
export interface ILoadingViewHandler {
|
20
22
|
onLoadingBegin(message?: string)
|
21
23
|
onLoadingUpdate(progress: LoadingProgressArgs | number, message?: string);
|
@@ -25,6 +27,7 @@
|
|
25
27
|
|
26
28
|
let currentFileProgress = 0;
|
27
29
|
let currentFileName: string;
|
30
|
+
/** @internal */
|
28
31
|
export function calculateProgress01(progress: LoadingProgressArgs) {
|
29
32
|
if (debug) console.log(progress.progress.loaded.toFixed(0) + "/" + progress.progress.total.toFixed(0), progress);
|
30
33
|
|
@@ -48,6 +51,7 @@
|
|
48
51
|
return Mathf.clamp01(prog);
|
49
52
|
}
|
50
53
|
|
54
|
+
/** @internal */
|
51
55
|
export class EngineLoadingView implements ILoadingViewHandler {
|
52
56
|
|
53
57
|
static LoadingContainerClassName = "loading";
|
@@ -6,6 +6,7 @@
|
|
6
6
|
export const quitARClassName = "quit-ar";
|
7
7
|
|
8
8
|
// https://developers.google.com/web/fundamentals/web-components/customelements
|
9
|
+
/** @internal */
|
9
10
|
export class AROverlayHandler {
|
10
11
|
|
11
12
|
get ARContainer(): HTMLElement | null { return this.arContainer; }
|
@@ -38,7 +38,8 @@
|
|
38
38
|
]
|
39
39
|
|
40
40
|
// https://developers.google.com/web/fundamentals/web-components/customelements
|
41
|
-
|
41
|
+
|
42
|
+
/** <needle-engine> web component. See {@link NeedleEngineAttributes} attributes for supported attributes
|
42
43
|
* @type {import ("./engine_element_attributes.js").NeedleEngineAttributes}
|
43
44
|
*/
|
44
45
|
export class NeedleEngineHTMLElement extends HTMLElement implements INeedleEngineComponent {
|
@@ -17,6 +17,7 @@
|
|
17
17
|
const debug = getParam("debuggetcomponent");
|
18
18
|
const debugInstantiate = getParam("debuginstantiate");
|
19
19
|
|
20
|
+
/** @internal */
|
20
21
|
export enum HideFlags {
|
21
22
|
None = 0,
|
22
23
|
HideInHierarchy = 1,
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { AxesHelper,Box3, BoxGeometry, BufferAttribute, Color, type ColorRepresentation, CylinderGeometry, EdgesGeometry, Line, LineBasicMaterial, LineSegments, Mesh, Object3D, Quaternion, SphereGeometry, Vector3 } from 'three';
|
1
|
+
import { AxesHelper, Box3, BoxGeometry, BufferAttribute, Color, type ColorRepresentation, CylinderGeometry, EdgesGeometry, Line, LineBasicMaterial, LineSegments, Mesh, Object3D, Quaternion, SphereGeometry, Vector3 } from 'three';
|
2
2
|
import ThreeMeshUI, { Inline, Text } from "three-mesh-ui"
|
3
3
|
import { type Options } from 'three-mesh-ui/build/types/core/elements/MeshUIBaseElement.js';
|
4
4
|
|
@@ -21,8 +21,14 @@
|
|
21
21
|
}
|
22
22
|
declare type ColorWithAlpha = Color & { a: number };
|
23
23
|
|
24
|
+
/** Gizmos are temporary objects that are drawn in the scene for debugging or visualization purposes
|
25
|
+
* They are automatically removed after a given duration and cached internally to reduce overhead.
|
26
|
+
* Use the static methods of this class to draw gizmos in the scene.
|
27
|
+
*/
|
24
28
|
export class Gizmos {
|
25
29
|
|
30
|
+
private constructor() { }
|
31
|
+
|
26
32
|
/**
|
27
33
|
* Allow creating gizmos
|
28
34
|
* If disabled then no gizmos will be added to the scene anymore
|
@@ -51,7 +57,7 @@
|
|
51
57
|
element.position.z = position.z;
|
52
58
|
return element as LabelHandle;
|
53
59
|
}
|
54
|
-
|
60
|
+
|
55
61
|
static DrawRay(origin: Vec3, dir: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
|
56
62
|
if (!Gizmos.enabled) return;
|
57
63
|
const obj = Internal.getLine(duration);
|
@@ -4,11 +4,34 @@
|
|
4
4
|
|
5
5
|
const onXRSessionStartListeners: ((evt: XRSessionEventArgs) => void)[] = [];
|
6
6
|
|
7
|
+
/**
|
8
|
+
* Add a listener for when an XR session starts
|
9
|
+
* This event is triggered when the XR session is started, either by the user or by the application
|
10
|
+
* @param fn The function to call when the XR session starts
|
11
|
+
* @example
|
12
|
+
* ```js
|
13
|
+
* onXRSessionStart((evt) => {
|
14
|
+
* console.log("XR session started", evt);
|
15
|
+
* });
|
16
|
+
* ```
|
17
|
+
*/
|
7
18
|
export function onXRSessionStart(fn: (evt: XRSessionEventArgs) => void) {
|
8
19
|
if (onXRSessionStartListeners.indexOf(fn) === -1) {
|
9
20
|
onXRSessionStartListeners.push(fn);
|
10
21
|
}
|
11
22
|
}
|
23
|
+
/**
|
24
|
+
* Remove a listener for when an XR session starts
|
25
|
+
* @param fn The function to remove from the listeners
|
26
|
+
* @example
|
27
|
+
* ```js
|
28
|
+
* const myFunction = (evt) => {
|
29
|
+
* console.log("XR session started", evt);
|
30
|
+
* };
|
31
|
+
* onXRSessionStart(myFunction);
|
32
|
+
* offXRSessionStart(myFunction);
|
33
|
+
* ```
|
34
|
+
*/
|
12
35
|
export function offXRSessionStart(fn: (evt: XRSessionEventArgs) => void) {
|
13
36
|
const index = onXRSessionStartListeners.indexOf(fn);
|
14
37
|
if (index !== -1) {
|
@@ -16,7 +39,50 @@
|
|
16
39
|
}
|
17
40
|
}
|
18
41
|
|
42
|
+
const onXRSessionEndListeners: ((evt: XRSessionEventArgs) => void)[] = [];
|
19
43
|
|
44
|
+
/**
|
45
|
+
* Add a listener for when an XR session ends
|
46
|
+
* This event is triggered when the XR session is ended, either by the user or by the application
|
47
|
+
* @param fn The function to call when the XR session ends
|
48
|
+
* @example
|
49
|
+
* ```js
|
50
|
+
* onXRSessionEnd((evt) => {
|
51
|
+
* console.log("XR session ended", evt);
|
52
|
+
* });
|
53
|
+
* ```
|
54
|
+
*/
|
55
|
+
export function onXRSessionEnd(fn: (evt: XRSessionEventArgs) => void) {
|
56
|
+
if (onXRSessionEndListeners.indexOf(fn) === -1) {
|
57
|
+
onXRSessionEndListeners.push(fn);
|
58
|
+
}
|
59
|
+
};
|
60
|
+
|
61
|
+
/**
|
62
|
+
* Remove a listener for when an XR session ends
|
63
|
+
* @param fn The function to remove from the listeners
|
64
|
+
* @example
|
65
|
+
* ```js
|
66
|
+
* const myFunction = (evt) => {
|
67
|
+
* console.log("XR session ended", evt);
|
68
|
+
* };
|
69
|
+
* onXRSessionEnd(myFunction);
|
70
|
+
* offXRSessionEnd(myFunction);
|
71
|
+
* ```
|
72
|
+
*/
|
73
|
+
export function offXRSessionEnd(fn: (evt: XRSessionEventArgs) => void) {
|
74
|
+
const index = onXRSessionEndListeners.indexOf(fn);
|
75
|
+
if (index !== -1) {
|
76
|
+
onXRSessionEndListeners.splice(index, 1);
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
|
81
|
+
/**
|
82
|
+
* @internal
|
83
|
+
* Invoke the XRSessionStart event
|
84
|
+
* @param evt The XRSession event arguments
|
85
|
+
*/
|
20
86
|
export function invokeXRSessionStart(evt: XRSessionEventArgs) {
|
21
87
|
globalThis.dispatchEvent(new CustomEvent("needle-xrsession-start", { detail: evt }));
|
22
88
|
for (let i = 0; i < onXRSessionStartListeners.length; i++) {
|
@@ -24,6 +90,14 @@
|
|
24
90
|
}
|
25
91
|
}
|
26
92
|
|
93
|
+
/**
|
94
|
+
* @internal
|
95
|
+
* Invoke the XRSessionEnd event
|
96
|
+
* @param evt The XRSession event arguments
|
97
|
+
*/
|
27
98
|
export function invokeXRSessionEnd(evt: XRSessionEventArgs) {
|
28
99
|
globalThis.dispatchEvent(new CustomEvent("needle-xrsession-end", { detail: evt }));
|
100
|
+
for (let i = 0; i < onXRSessionEndListeners.length; i++) {
|
101
|
+
onXRSessionEndListeners[i](evt);
|
102
|
+
}
|
29
103
|
}
|
@@ -1,88 +1,23 @@
|
|
1
1
|
|
2
|
+
/**
|
3
|
+
* @internal
|
4
|
+
*/
|
2
5
|
export enum EventType {
|
3
|
-
|
4
|
-
/// <summary>
|
5
|
-
/// Intercepts a IPointerEnterHandler.OnPointerEnter.
|
6
|
-
/// </summary>
|
7
6
|
PointerEnter = 0,
|
8
|
-
|
9
|
-
/// <summary>
|
10
|
-
/// Intercepts a IPointerExitHandler.OnPointerExit.
|
11
|
-
/// </summary>
|
12
7
|
PointerExit = 1,
|
13
|
-
|
14
|
-
/// <summary>
|
15
|
-
/// Intercepts a IPointerDownHandler.OnPointerDown.
|
16
|
-
/// </summary>
|
17
8
|
PointerDown = 2,
|
18
|
-
|
19
|
-
/// <summary>
|
20
|
-
/// Intercepts a IPointerUpHandler.OnPointerUp.
|
21
|
-
/// </summary>
|
22
9
|
PointerUp = 3,
|
23
|
-
|
24
|
-
/// <summary>
|
25
|
-
/// Intercepts a IPointerClickHandler.OnPointerClick.
|
26
|
-
/// </summary>
|
27
10
|
PointerClick = 4,
|
28
|
-
|
29
|
-
/// <summary>
|
30
|
-
/// Intercepts a IDragHandler.OnDrag.
|
31
|
-
/// </summary>
|
32
11
|
Drag = 5,
|
33
|
-
|
34
|
-
/// <summary>
|
35
|
-
/// Intercepts a IDropHandler.OnDrop.
|
36
|
-
/// </summary>
|
37
12
|
Drop = 6,
|
38
|
-
|
39
|
-
/// <summary>
|
40
|
-
/// Intercepts a IScrollHandler.OnScroll.
|
41
|
-
/// </summary>
|
42
13
|
Scroll = 7,
|
43
|
-
|
44
|
-
/// <summary>
|
45
|
-
/// Intercepts a IUpdateSelectedHandler.OnUpdateSelected.
|
46
|
-
/// </summary>
|
47
14
|
UpdateSelected = 8,
|
48
|
-
|
49
|
-
/// <summary>
|
50
|
-
/// Intercepts a ISelectHandler.OnSelect.
|
51
|
-
/// </summary>
|
52
15
|
Select = 9,
|
53
|
-
|
54
|
-
/// <summary>
|
55
|
-
/// Intercepts a IDeselectHandler.OnDeselect.
|
56
|
-
/// </summary>
|
57
16
|
Deselect = 10,
|
58
|
-
|
59
|
-
/// <summary>
|
60
|
-
/// Intercepts a IMoveHandler.OnMove.
|
61
|
-
/// </summary>
|
62
17
|
Move = 11,
|
63
|
-
|
64
|
-
/// <summary>
|
65
|
-
/// Intercepts IInitializePotentialDrag.InitializePotentialDrag.
|
66
|
-
/// </summary>
|
67
18
|
InitializePotentialDrag = 12,
|
68
|
-
|
69
|
-
/// <summary>
|
70
|
-
/// Intercepts IBeginDragHandler.OnBeginDrag.</
|
71
|
-
/// </summary>
|
72
19
|
BeginDrag = 13,
|
73
|
-
|
74
|
-
/// <summary>
|
75
|
-
/// Intercepts IEndDragHandler.OnEndDrag.
|
76
|
-
/// </summary>
|
77
20
|
EndDrag = 14,
|
78
|
-
|
79
|
-
/// <summary>
|
80
|
-
/// Intercepts ISubmitHandler.Submit.
|
81
|
-
/// </summary>
|
82
21
|
Submit = 15,
|
83
|
-
|
84
|
-
/// <summary>
|
85
|
-
/// Intercepts ICancelHandler.OnCancel.
|
86
|
-
/// </summary>
|
87
22
|
Cancel = 16
|
88
23
|
}
|
@@ -4,6 +4,7 @@
|
|
4
4
|
|
5
5
|
const handlers: Map<any, ApplyPrototypeExtension> = new Map();
|
6
6
|
|
7
|
+
/** @internal */
|
7
8
|
export function applyPrototypeExtensions<T>(obj: any, prototype : Constructor<T>) {
|
8
9
|
if (!obj) return;
|
9
10
|
// const prototype = Object.getPrototypeOf(obj);
|
@@ -20,6 +21,7 @@
|
|
20
21
|
// applyPrototypeExtensions(prototype);
|
21
22
|
}
|
22
23
|
|
24
|
+
/** @internal */
|
23
25
|
export function registerPrototypeExtensions<T>(type: Constructor<T>) {
|
24
26
|
// console.log("Register", type.prototype.constructor.name);
|
25
27
|
const handler = createPrototypeExtensionHandler(type.prototype);
|
@@ -31,6 +33,7 @@
|
|
31
33
|
return new ApplyPrototypeExtension(prototype);
|
32
34
|
}
|
33
35
|
|
36
|
+
/** @internal */
|
34
37
|
export interface IApplyPrototypeExtension {
|
35
38
|
apply(object: object): void;
|
36
39
|
}
|
@@ -4,13 +4,13 @@
|
|
4
4
|
import { Behaviour } from "./Component.js";
|
5
5
|
|
6
6
|
|
7
|
-
|
8
|
-
|
7
|
+
enum FogMode {
|
9
8
|
Linear = 1,
|
10
9
|
Exponential = 2,
|
11
10
|
ExponentialSquared = 3,
|
12
11
|
}
|
13
12
|
|
13
|
+
/** @internal */
|
14
14
|
export class Fog extends Behaviour {
|
15
15
|
|
16
16
|
get fog() {
|
@@ -4,6 +4,9 @@
|
|
4
4
|
import { serializable } from "../engine/engine_serialization_decorator.js";
|
5
5
|
import { Behaviour } from "./Component.js";
|
6
6
|
|
7
|
+
/**
|
8
|
+
* GridHelper is a component that allows to display a grid in the scene.
|
9
|
+
*/
|
7
10
|
export class GridHelper extends Behaviour {
|
8
11
|
|
9
12
|
@serializable()
|
@@ -18,6 +21,7 @@
|
|
18
21
|
private divisions!: number;
|
19
22
|
private offset!: number;
|
20
23
|
|
24
|
+
/** @internal */
|
21
25
|
onEnable() {
|
22
26
|
if (this.isGizmo && !params.showGizmos) return;
|
23
27
|
|
@@ -32,6 +36,7 @@
|
|
32
36
|
this.gameObject.add(this.gridHelper);
|
33
37
|
}
|
34
38
|
|
39
|
+
/** @internal */
|
35
40
|
onDisable(): void {
|
36
41
|
if (this.gridHelper) {
|
37
42
|
this.gameObject.remove(this.gridHelper);
|
@@ -7,6 +7,9 @@
|
|
7
7
|
|
8
8
|
const debug = getParam("debuggroundprojection");
|
9
9
|
|
10
|
+
/**
|
11
|
+
* GroundProjectedEnv creates a ground projection of the current environment map.
|
12
|
+
*/
|
10
13
|
export class GroundProjectedEnv extends Behaviour {
|
11
14
|
|
12
15
|
@serializable()
|
@@ -22,7 +22,7 @@
|
|
22
22
|
|
23
23
|
private *create() {
|
24
24
|
yield;
|
25
|
-
if (this.rigidBody && this.connectedBody) {
|
25
|
+
if (this.rigidBody && this.connectedBody && this.activeAndEnabled) {
|
26
26
|
this.createJoint(this.rigidBody, this.connectedBody)
|
27
27
|
}
|
28
28
|
}
|
@@ -38,6 +38,9 @@
|
|
38
38
|
distance: number;
|
39
39
|
}
|
40
40
|
|
41
|
+
/**
|
42
|
+
* LODGroup allows to create a group of LOD levels for an object.
|
43
|
+
*/
|
41
44
|
export class LODGroup extends Behaviour {
|
42
45
|
|
43
46
|
fadeMode: LODFadeMode = LODFadeMode.None;
|
@@ -7,22 +7,39 @@
|
|
7
7
|
import { USDObject } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
|
8
8
|
import { Behaviour } from "../Component.js";
|
9
9
|
|
10
|
+
/**
|
11
|
+
* LookAt behaviour makes the object look at a target object or the camera.
|
12
|
+
* It can also invert the forward direction and keep the up direction.
|
13
|
+
*/
|
10
14
|
export class LookAt extends Behaviour implements UsdzBehaviour {
|
11
15
|
|
16
|
+
/**
|
17
|
+
* The target object to look at. If not set, the main camera will be used.
|
18
|
+
*/
|
12
19
|
@serializable(Object3D)
|
13
20
|
target?: Object3D;
|
14
21
|
|
22
|
+
/**
|
23
|
+
* Inverts the forward direction.
|
24
|
+
*/
|
15
25
|
@serializable()
|
16
26
|
invertForward: boolean = false;
|
17
27
|
|
28
|
+
/**
|
29
|
+
* Keep the up direction.
|
30
|
+
*/
|
18
31
|
@serializable()
|
19
32
|
keepUpDirection: boolean = true;
|
20
33
|
|
34
|
+
/**
|
35
|
+
* Copy the target rotation.
|
36
|
+
*/
|
21
37
|
@serializable()
|
22
38
|
copyTargetRotation: boolean = false;
|
23
39
|
|
24
40
|
private static flipYQuat: Quaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
|
25
41
|
|
42
|
+
/** @internal */
|
26
43
|
onBeforeRender(): void {
|
27
44
|
let target: Object3D | null | undefined = this.target;
|
28
45
|
if (!target) target = this.context.mainCamera;
|
@@ -34,6 +51,7 @@
|
|
34
51
|
this.gameObject.quaternion.multiply(LookAt.flipYQuat);
|
35
52
|
}
|
36
53
|
|
54
|
+
/** @internal */
|
37
55
|
createBehaviours(ext, model: USDObject, _context) {
|
38
56
|
if (model.uuid === this.gameObject.uuid) {
|
39
57
|
let alignmentTarget = model;
|
@@ -34,13 +34,14 @@
|
|
34
34
|
const cur = obj[key];
|
35
35
|
if (cur instanceof BufferGeometry) {
|
36
36
|
const info = NEEDLE_progressive.getMeshLODInformation(cur);
|
37
|
-
const level = !info ? 0 : Math.min(currentDebugLodLevel, info.lods.length
|
37
|
+
const level = !info ? 0 : Math.min(currentDebugLodLevel, info.lods.length);
|
38
38
|
obj["DEBUG:LOD"] = level;
|
39
39
|
NEEDLE_progressive.assignMeshLOD(context, arr.sourceId, obj as Mesh, level);
|
40
|
-
if(info) maxLevel = Math.max(maxLevel, info.lods.length);
|
40
|
+
if (info) maxLevel = Math.max(maxLevel, info.lods.length - 1);
|
41
41
|
}
|
42
|
-
else if (
|
43
|
-
NEEDLE_progressive.assignTextureLOD(context, arr.sourceId,
|
42
|
+
else if (obj instanceof Material) {
|
43
|
+
NEEDLE_progressive.assignTextureLOD(context, arr.sourceId, obj, currentDebugLodLevel);
|
44
|
+
break;
|
44
45
|
}
|
45
46
|
}
|
46
47
|
});
|
@@ -388,6 +389,7 @@
|
|
388
389
|
const LODLEVEL = ext.lods.length;
|
389
390
|
if (obj instanceof Mesh) {
|
390
391
|
applyMeshLOD(LODKEY, obj, LODLEVEL, undefined, ext);
|
392
|
+
NEEDLE_progressive.lowresCache.set(LODKEY, obj.geometry);
|
391
393
|
}
|
392
394
|
else {
|
393
395
|
const geometries = new Array<BufferGeometry>();
|
@@ -1,7 +1,10 @@
|
|
1
1
|
import { serializable } from '../engine/engine_serialization.js';
|
2
2
|
import { Behaviour } from './Component.js';
|
3
3
|
|
4
|
-
/**
|
4
|
+
/**
|
5
|
+
* Exposes options to editors to customize the Needle menu.
|
6
|
+
* From code you can access the menu via `{@link Context.menu}`
|
7
|
+
**/
|
5
8
|
export class NeedleMenu extends Behaviour {
|
6
9
|
|
7
10
|
@serializable()
|
@@ -526,6 +526,7 @@
|
|
526
526
|
|
527
527
|
/**
|
528
528
|
* @link https://developer.mozilla.org/en-US/docs/Web/API/XRSession/visibilityState
|
529
|
+
* @returns {XRVisibilityState} The visibility state of the XRSession
|
529
530
|
*/
|
530
531
|
get visibilityState() { return this.session.visibilityState; }
|
531
532
|
|
@@ -19,7 +19,10 @@
|
|
19
19
|
import { applyPrototypeExtensions, registerPrototypeExtensions } from "./ExtensionUtils.js";
|
20
20
|
|
21
21
|
|
22
|
-
/**
|
22
|
+
/**
|
23
|
+
* @internal
|
24
|
+
* used to decorate cloned object3D objects with the same added components defined above
|
25
|
+
**/
|
23
26
|
export function apply(object: Object3D) {
|
24
27
|
if (object && object.isObject3D === true) {
|
25
28
|
applyPrototypeExtensions(object, Object3D);
|
@@ -6,23 +6,41 @@
|
|
6
6
|
import { type IPointerClickHandler, PointerEventData } from "../ui/index.js";
|
7
7
|
import { ObjectRaycaster, Raycaster } from "../ui/Raycaster.js";
|
8
8
|
|
9
|
+
/**
|
10
|
+
* OpenURLMode defines how a URL should be opened.
|
11
|
+
*/
|
9
12
|
export enum OpenURLMode {
|
10
13
|
NewTab = 0,
|
11
14
|
SameTab = 1,
|
12
15
|
NewWindow = 2
|
13
16
|
}
|
14
17
|
|
18
|
+
/**
|
19
|
+
* OpenURL behaviour opens a URL in a new tab or window.
|
20
|
+
*/
|
15
21
|
export class OpenURL extends Behaviour implements IPointerClickHandler {
|
16
22
|
|
23
|
+
/**
|
24
|
+
* The URL to open.
|
25
|
+
*/
|
17
26
|
@serializable()
|
18
|
-
clickable: boolean = true;
|
19
|
-
|
20
|
-
@serializable()
|
21
27
|
url?: string;
|
22
28
|
|
29
|
+
/**
|
30
|
+
* The mode in which the URL should be opened: NewTab, SameTab, NewWindow.
|
31
|
+
*/
|
23
32
|
@serializable()
|
24
33
|
mode: OpenURLMode = OpenURLMode.NewTab;
|
25
34
|
|
35
|
+
/**
|
36
|
+
* If true, the URL will be opened when the object with this component is clicked.
|
37
|
+
*/
|
38
|
+
@serializable()
|
39
|
+
clickable: boolean = true;
|
40
|
+
|
41
|
+
/**
|
42
|
+
* Opens the URL in a new tab or window.
|
43
|
+
*/
|
26
44
|
async open() {
|
27
45
|
if (!this.url) {
|
28
46
|
console.warn("OpenURL: URL is not set, can't open.", this);
|
@@ -63,18 +81,22 @@
|
|
63
81
|
|
64
82
|
}
|
65
83
|
}
|
84
|
+
/** @internal */
|
66
85
|
start(): void {
|
67
86
|
const raycaster = this.gameObject.getComponentInParent(ObjectRaycaster);
|
68
|
-
if (!raycaster) this.gameObject.
|
87
|
+
if (!raycaster) this.gameObject.addComponent(ObjectRaycaster);
|
69
88
|
}
|
89
|
+
/** @internal */
|
70
90
|
onPointerEnter(args) {
|
71
91
|
if (!args.used && this.clickable)
|
72
92
|
this.context.input.setCursorPointer();
|
73
93
|
}
|
94
|
+
/** @internal */
|
74
95
|
onPointerExit() {
|
75
96
|
if (this.clickable)
|
76
97
|
this.context.input.setCursorNormal();
|
77
98
|
}
|
99
|
+
/** @internal */
|
78
100
|
onPointerClick(args: PointerEventData) {
|
79
101
|
if (this.clickable && !args.used && this.url?.length)
|
80
102
|
this.open();
|
@@ -14,9 +14,9 @@
|
|
14
14
|
|
15
15
|
const debug = getParam("debugtimeline");
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
/**
|
18
|
+
* The wrap mode of the {@link PlayableDirector}.
|
19
|
+
*/
|
20
20
|
export enum DirectorWrapMode {
|
21
21
|
/// <summary>
|
22
22
|
/// <para>Hold the last frame when the playable time reaches it's duration.</para>
|
@@ -32,34 +32,29 @@
|
|
32
32
|
None = 2,
|
33
33
|
}
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
/// </summary>
|
35
|
+
|
36
|
+
/** How the clip handles time outside its start and end range. */
|
38
37
|
export enum ClipExtrapolation {
|
39
|
-
|
40
|
-
/// No extrapolation is applied.
|
41
|
-
/// </summary>
|
38
|
+
/** No extrapolation is applied. */
|
42
39
|
None = 0,
|
43
|
-
|
44
|
-
/// Hold the time at the end value of the clip.
|
45
|
-
/// </summary>
|
40
|
+
/** Hold the time at the end value of the clip. */
|
46
41
|
Hold = 1,
|
47
|
-
|
48
|
-
/// Repeat time values outside the start/end range.
|
49
|
-
/// </summary>
|
42
|
+
/** Repeat time values outside the start/end range. */
|
50
43
|
Loop = 2,
|
51
|
-
|
52
|
-
/// Repeat time values outside the start/end range, reversing direction at each loop
|
53
|
-
/// </summary>
|
44
|
+
/** Repeat time values outside the start/end range, reversing direction at each loop */
|
54
45
|
PingPong = 3,
|
55
|
-
|
56
|
-
/// Time values are passed in without modification, extending beyond the clips range
|
57
|
-
/// </summary>
|
46
|
+
/** Time values are passed in without modification, extending beyond the clips range */
|
58
47
|
Continue = 4
|
59
48
|
};
|
60
49
|
|
50
|
+
/** @internal */
|
61
51
|
export type CreateTrackFunction = (director: PlayableDirector, track: Models.TrackModel) => Tracks.TrackHandler | undefined | null;
|
62
52
|
|
53
|
+
/**
|
54
|
+
* The PlayableDirector component is the main component to control timelines in needle engine.
|
55
|
+
* It is used to play, pause, stop and evaluate timelines.
|
56
|
+
* Assign a TimelineAsset to the `playableAsset` property to start playing a timeline.
|
57
|
+
*/
|
63
58
|
export class PlayableDirector extends Behaviour {
|
64
59
|
|
65
60
|
private static createTrackFunctions: { [key: string]: CreateTrackFunction } = {};
|
@@ -68,11 +63,15 @@
|
|
68
63
|
}
|
69
64
|
|
70
65
|
playableAsset?: Models.TimelineAssetModel;
|
66
|
+
/** Set to true to start playing the timeline when the scene starts */
|
71
67
|
playOnAwake?: boolean;
|
72
68
|
extrapolationMode: DirectorWrapMode = DirectorWrapMode.Loop;
|
73
69
|
|
70
|
+
/** @returns true if the timeline is currently playing */
|
74
71
|
get isPlaying(): boolean { return this._isPlaying; }
|
72
|
+
/** @returns true if the timeline is currently paused */
|
75
73
|
get isPaused(): boolean { return this._isPaused; }
|
74
|
+
/** the current time of the timeline */
|
76
75
|
get time(): number { return this._time; }
|
77
76
|
set time(value: number) {
|
78
77
|
if (typeof value === "number" && !Number.isNaN(value))
|
@@ -81,10 +80,13 @@
|
|
81
80
|
console.error("INVALID TIMELINE.TIME VALUE", value, this.name)
|
82
81
|
};
|
83
82
|
}
|
83
|
+
/** the duration of the timeline */
|
84
84
|
get duration(): number { return this._duration; }
|
85
85
|
set duration(value: number) { this._duration = value; }
|
86
|
+
/** the weight of the timeline. Set to a value below 1 to blend with other timelines */
|
86
87
|
get weight(): number { return this._weight; };
|
87
88
|
set weight(value: number) { this._weight = value; }
|
89
|
+
/** the playback speed of the timeline */
|
88
90
|
get speed(): number { return this._speed; }
|
89
91
|
set speed(value: number) { this._speed = value; }
|
90
92
|
|
@@ -95,6 +97,7 @@
|
|
95
97
|
private _clonedPlayableAsset: boolean = false;
|
96
98
|
private _speed: number = 1;
|
97
99
|
|
100
|
+
/** @internal */
|
98
101
|
awake(): void {
|
99
102
|
if (debug)
|
100
103
|
console.log(this, this.playableAsset?.tracks);
|
@@ -104,6 +107,7 @@
|
|
104
107
|
if (!this.isValid()) console.warn("PlayableDirector is not valid", this.playableAsset, this.playableAsset?.tracks, Array.isArray(this.playableAsset?.tracks), this);
|
105
108
|
}
|
106
109
|
|
110
|
+
/** @internal */
|
107
111
|
onEnable() {
|
108
112
|
for (const track of this._audioTracks) {
|
109
113
|
track.onEnable?.();
|
@@ -131,6 +135,7 @@
|
|
131
135
|
window.addEventListener('visibilitychange', this._visibilityChangeEvt);
|
132
136
|
}
|
133
137
|
|
138
|
+
/** @internal */
|
134
139
|
onDisable(): void {
|
135
140
|
this.stop();
|
136
141
|
for (const track of this._audioTracks) {
|
@@ -146,6 +151,7 @@
|
|
146
151
|
window.removeEventListener('visibilitychange', this._visibilityChangeEvt);
|
147
152
|
}
|
148
153
|
|
154
|
+
/** @internal */
|
149
155
|
onDestroy(): void {
|
150
156
|
for (const tracks of this._allTracks) {
|
151
157
|
for (const track of tracks)
|
@@ -153,6 +159,7 @@
|
|
153
159
|
}
|
154
160
|
}
|
155
161
|
|
162
|
+
/** @internal */
|
156
163
|
rebuildGraph() {
|
157
164
|
if (!this.isValid()) return;
|
158
165
|
this.resolveBindings();
|
@@ -160,6 +167,10 @@
|
|
160
167
|
this.setupAndCreateTrackHandlers();
|
161
168
|
}
|
162
169
|
|
170
|
+
/**
|
171
|
+
* Play the timeline from the current time.
|
172
|
+
* If the timeline is already playing this method does nothing.
|
173
|
+
*/
|
163
174
|
async play() {
|
164
175
|
if (!this.isValid()) return;
|
165
176
|
const pauseChanged = this._isPaused == true;
|
@@ -190,6 +201,9 @@
|
|
190
201
|
this._internalUpdateRoutine = this.startCoroutine(this.internalUpdate(), FrameEvent.LateUpdate);
|
191
202
|
}
|
192
203
|
|
204
|
+
/**
|
205
|
+
* Pause the timeline.
|
206
|
+
*/
|
193
207
|
pause() {
|
194
208
|
if (!this.isValid()) return;
|
195
209
|
this._isPlaying = false;
|
@@ -200,6 +214,9 @@
|
|
200
214
|
this.invokeStateChangedMethodsOnTracks();
|
201
215
|
}
|
202
216
|
|
217
|
+
/**
|
218
|
+
* Stop the timeline.
|
219
|
+
*/
|
203
220
|
stop() {
|
204
221
|
this._isStopping = true;
|
205
222
|
for (const track of this._audioTracks) track.stop();
|
@@ -222,14 +239,23 @@
|
|
222
239
|
this._isStopping = false;
|
223
240
|
}
|
224
241
|
|
242
|
+
/**
|
243
|
+
* Evaluate the timeline at the current time. This is useful when you want to manually update the timeline e.g. when the timeline is paused and you set `time` to a new value.
|
244
|
+
*/
|
225
245
|
evaluate() {
|
226
246
|
this.internalEvaluate(true);
|
227
247
|
}
|
228
248
|
|
249
|
+
/**
|
250
|
+
* @returns true if the timeline is valid and has tracks
|
251
|
+
*/
|
229
252
|
isValid() {
|
230
253
|
return this.playableAsset && this.playableAsset.tracks && Array.isArray(this.playableAsset.tracks);
|
231
254
|
}
|
232
255
|
|
256
|
+
/** Iterates over all tracks of the timeline
|
257
|
+
* @returns all tracks of the timeline
|
258
|
+
*/
|
233
259
|
*forEachTrack() {
|
234
260
|
for (const tracks of this._allTracks) {
|
235
261
|
for (const track of tracks)
|
@@ -237,11 +263,15 @@
|
|
237
263
|
}
|
238
264
|
}
|
239
265
|
|
266
|
+
/**
|
267
|
+
* @returns all audio tracks of the timeline
|
268
|
+
*/
|
240
269
|
get audioTracks(): Tracks.AudioTrackHandler[] {
|
241
270
|
return this._audioTracks;
|
242
271
|
}
|
243
272
|
|
244
273
|
private _guidsMap?: GuidsMap;
|
274
|
+
/** @internal */
|
245
275
|
resolveGuids(map: GuidsMap) {
|
246
276
|
this._guidsMap = map;
|
247
277
|
}
|
@@ -561,7 +591,7 @@
|
|
561
591
|
audio.director = this;
|
562
592
|
audio.track = track;
|
563
593
|
audio.audioSource = track.outputs.find(o => o instanceof AudioSource) as AudioSource;
|
564
|
-
|
594
|
+
|
565
595
|
this._audioTracks.push(audio);
|
566
596
|
if (!audioListener) {
|
567
597
|
// If the scene doesnt have an AudioListener we add one to the main camera
|
@@ -6,7 +6,10 @@
|
|
6
6
|
import { Behaviour, GameObject } from "./Component.js";
|
7
7
|
import { AvatarMarker } from "./webxr/WebXRAvatar.js";
|
8
8
|
|
9
|
-
|
9
|
+
/**
|
10
|
+
* PlayerColor assigns a unique color for each user in the room to the object it is attached to.
|
11
|
+
* The color is generated based on the user's ID.
|
12
|
+
*/
|
10
13
|
export class PlayerColor extends Behaviour {
|
11
14
|
|
12
15
|
private _didAssignPlayerColor: boolean = false;
|
@@ -16,6 +16,32 @@
|
|
16
16
|
unapply(): void;
|
17
17
|
}
|
18
18
|
|
19
|
+
/**
|
20
|
+
* PostProcessingEffect is a base class for post processing effects that can be applied to the scene.
|
21
|
+
* To create a custom post processing effect, extend this class and override the `onCreateEffect` method and call `registerCustomEffectType` to make it available in the editor.
|
22
|
+
* @example
|
23
|
+
* ```typescript
|
24
|
+
* import { EdgeDetectionMode, SMAAEffect, SMAAPreset } from "postprocessing";
|
25
|
+
* export class Antialiasing extends PostProcessingEffect {
|
26
|
+
* get typeName(): string {
|
27
|
+
* return "Antialiasing";
|
28
|
+
* }
|
29
|
+
* @serializable(VolumeParameter)
|
30
|
+
* preset!: VolumeParameter = new VolumeParameter();
|
31
|
+
* onCreateEffect(): EffectProviderResult {
|
32
|
+
* const effect = new SMAAEffect({
|
33
|
+
* preset: SMAAPreset.HIGH,
|
34
|
+
* edgeDetectionMode: EdgeDetectionMode.DEPTH
|
35
|
+
* });
|
36
|
+
* this.preset.onValueChanged = (newValue) => {
|
37
|
+
* effect.applyPreset(newValue);
|
38
|
+
* };
|
39
|
+
* return effect;
|
40
|
+
* }
|
41
|
+
* }
|
42
|
+
* registerCustomEffectType("Antialiasing", Antialiasing)
|
43
|
+
* ```
|
44
|
+
*/
|
19
45
|
export abstract class PostProcessingEffect extends Component implements IEffectProvider, ISerializable, IEditorModification {
|
20
46
|
|
21
47
|
constructor(params: any = undefined) {
|
@@ -14,6 +14,9 @@
|
|
14
14
|
const activeKey = Symbol("needle:postprocessing-handler");
|
15
15
|
const autoclearSetting = Symbol("needle:previous-autoclear-state")
|
16
16
|
|
17
|
+
/**
|
18
|
+
* PostProcessingHandler is responsible for applying post processing effects to the scene. It is internally used by the {@link Volume} component
|
19
|
+
*/
|
17
20
|
export class PostProcessingHandler {
|
18
21
|
|
19
22
|
private _composer: EffectComposer | null = null;
|
@@ -1,6 +1,6 @@
|
|
1
1
|
/* eslint-disable */
|
2
2
|
import { TypeStore } from "./../engine_typestore.js"
|
3
|
-
|
3
|
+
|
4
4
|
// Import types
|
5
5
|
import { __Ignore } from "../../engine-components/codegen/components.js";
|
6
6
|
import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
|
@@ -92,6 +92,7 @@
|
|
92
92
|
import { InstanceHandle } from "../../engine-components/RendererInstancing.js";
|
93
93
|
import { InstancingHandler } from "../../engine-components/RendererInstancing.js";
|
94
94
|
import { Interactable } from "../../engine-components/Interactable.js";
|
95
|
+
import { Keyframe } from "../../engine-components/AnimationCurve.js";
|
95
96
|
import { Light } from "../../engine-components/Light.js";
|
96
97
|
import { LimitVelocityOverLifetimeModule } from "../../engine-components/ParticleSystemModules.js";
|
97
98
|
import { LODGroup } from "../../engine-components/LODGroup.js";
|
@@ -218,7 +219,7 @@
|
|
218
219
|
import { XRFlag } from "../../engine-components/webxr/XRFlag.js";
|
219
220
|
import { XRRig } from "../../engine-components/webxr/WebXRRig.js";
|
220
221
|
import { XRState } from "../../engine-components/webxr/XRFlag.js";
|
221
|
-
|
222
|
+
|
222
223
|
// Register types
|
223
224
|
TypeStore.add("__Ignore", __Ignore);
|
224
225
|
TypeStore.add("ActionBuilder", ActionBuilder);
|
@@ -310,6 +311,7 @@
|
|
310
311
|
TypeStore.add("InstanceHandle", InstanceHandle);
|
311
312
|
TypeStore.add("InstancingHandler", InstancingHandler);
|
312
313
|
TypeStore.add("Interactable", Interactable);
|
314
|
+
TypeStore.add("Keyframe", Keyframe);
|
313
315
|
TypeStore.add("Light", Light);
|
314
316
|
TypeStore.add("LimitVelocityOverLifetimeModule", LimitVelocityOverLifetimeModule);
|
315
317
|
TypeStore.add("LODGroup", LODGroup);
|
@@ -456,7 +456,7 @@
|
|
456
456
|
if (this.mustGrow()) {
|
457
457
|
this.grow(geo);
|
458
458
|
}
|
459
|
-
if (debugInstancing) console.
|
459
|
+
if (debugInstancing) console.debug("UPDATE MESH", index, geo.name, getMeshInformation(geo), geo.attributes.position.count, geo.index ? geo.index.count : 0);
|
460
460
|
this.inst.setGeometryAt(index, geo);
|
461
461
|
this.markNeedsUpdate();
|
462
462
|
return true;
|
@@ -633,7 +633,7 @@
|
|
633
633
|
if (smallestBucket != null) {
|
634
634
|
const bucket = smallestBucket;
|
635
635
|
if (debugInstancing)
|
636
|
-
console.
|
636
|
+
console.debug(`RE-USE SPACE #${bucket.index}, ${handle.maxVertexCount} vertices, ${handle.maxIndexCount} indices, ${handle.name}`);
|
637
637
|
this.inst.setGeometryAt(bucket.index, handle.object.geometry as BufferGeometry);
|
638
638
|
this.inst.setMatrixAt(bucket.index, handle.object.matrixWorld);
|
639
639
|
this.inst.setVisibleAt(bucket.index, true);
|
@@ -647,7 +647,7 @@
|
|
647
647
|
const geo = handle.object.geometry as BufferGeometry;
|
648
648
|
|
649
649
|
|
650
|
-
if (debugInstancing) console.
|
650
|
+
if (debugInstancing) console.debug("ADD GEOMETRY", geo.name, "\nvertex:", `${this._currentVertexCount} + ${handle.maxVertexCount} < ${this._maxVertexCount}?`, "\nindex:", handle.maxIndexCount, this._currentIndexCount, this._maxIndexCount);
|
651
651
|
|
652
652
|
const i = this.inst.addGeometry(geo, handle.maxVertexCount, handle.maxIndexCount);
|
653
653
|
handle.__instanceIndex = i;
|
@@ -658,7 +658,7 @@
|
|
658
658
|
this._usedBuckets[i] = { index: i, vertexCount: handle.maxVertexCount, indexCount: handle.maxIndexCount };
|
659
659
|
this.inst.setMatrixAt(i, handle.object.matrixWorld);
|
660
660
|
if (debugInstancing)
|
661
|
-
console.
|
661
|
+
console.debug(`ADD MESH & RESERVE SPACE #${i}, ${handle.maxVertexCount} vertices, ${handle.maxIndexCount} indices, ${handle.name} ${handle.object.uuid}`);
|
662
662
|
|
663
663
|
}
|
664
664
|
|
@@ -2,6 +2,9 @@
|
|
2
2
|
|
3
3
|
import { Mathf } from "../../engine/engine_math.js";
|
4
4
|
|
5
|
+
/**
|
6
|
+
* RGBAColor is a class that represents a color with red, green, blue and alpha components.
|
7
|
+
*/
|
5
8
|
export class RGBAColor extends Color {
|
6
9
|
alpha: number = 1;
|
7
10
|
|
@@ -18,6 +18,9 @@
|
|
18
18
|
reaction!: EventList;
|
19
19
|
}
|
20
20
|
|
21
|
+
/** SignalReceiver is a component that listens for signals and invokes a reaction when a signal is received.
|
22
|
+
* Signals can be added to a signal track on a timeline
|
23
|
+
*/
|
21
24
|
export class SignalReceiver extends Behaviour {
|
22
25
|
|
23
26
|
private static receivers: { [key: string]: SignalReceiver[] } = {};
|
@@ -36,10 +39,12 @@
|
|
36
39
|
@serializable(SignalReceiverEvent)
|
37
40
|
events?: SignalReceiverEvent[];
|
38
41
|
|
42
|
+
/** @internal */
|
39
43
|
awake(): void {
|
40
44
|
if(debug) console.log("SignalReceiver awake", this);
|
41
45
|
}
|
42
46
|
|
47
|
+
/** @internal */
|
43
48
|
onEnable(): void {
|
44
49
|
if (this.events) {
|
45
50
|
for (const evt of this.events) {
|
@@ -50,6 +55,7 @@
|
|
50
55
|
}
|
51
56
|
}
|
52
57
|
|
58
|
+
/** @internal */
|
53
59
|
onDisable(): void {
|
54
60
|
if (this.events) {
|
55
61
|
for (const evt of this.events) {
|
@@ -60,6 +60,10 @@
|
|
60
60
|
userId: string;
|
61
61
|
};
|
62
62
|
|
63
|
+
/**
|
64
|
+
* SyncedCamera is a component that syncs the camera position and rotation of all users in the room.
|
65
|
+
* A prefab can be set to represent the remote cameras visually in the scene.
|
66
|
+
*/
|
63
67
|
export class SyncedCamera extends Behaviour {
|
64
68
|
|
65
69
|
static instances: UserCamInfo[] = [];
|
@@ -70,6 +74,9 @@
|
|
70
74
|
return this.remoteCams[guid].obj;
|
71
75
|
}
|
72
76
|
|
77
|
+
/**
|
78
|
+
* The prefab to visually represent the remote cameras in the scene.
|
79
|
+
*/
|
73
80
|
@serializable([Object3D, AssetReference])
|
74
81
|
public cameraPrefab: Object3D | null | AssetReference = null;
|
75
82
|
|
@@ -84,6 +91,7 @@
|
|
84
91
|
private _camTimeoutInSeconds = 10;
|
85
92
|
private _receiveCallback: Function | null = null;
|
86
93
|
|
94
|
+
/** @internal */
|
87
95
|
async awake() {
|
88
96
|
this._lastWorldPosition = this.worldPosition.clone();
|
89
97
|
this._lastWorldQuaternion = this.worldQuaternion.clone();
|
@@ -101,14 +109,17 @@
|
|
101
109
|
|
102
110
|
}
|
103
111
|
|
112
|
+
/** @internal */
|
104
113
|
onEnable(): void {
|
105
114
|
this._receiveCallback = this.context.connection.beginListenBinary(SyncedCameraModelIdentifier, this.onReceivedRemoteCameraInfoBin.bind(this));
|
106
115
|
}
|
107
116
|
|
117
|
+
/** @internal */
|
108
118
|
onDisable(): void {
|
109
119
|
this.context.connection.stopListenBinary(SyncedCameraModelIdentifier, this._receiveCallback);
|
110
120
|
}
|
111
121
|
|
122
|
+
/** @internal */
|
112
123
|
update(): void {
|
113
124
|
|
114
125
|
for (const guid in this.remoteCams) {
|
@@ -10,28 +10,59 @@
|
|
10
10
|
const viewParamName = "view";
|
11
11
|
const debug = utils.getParam("debugsyncedroom");
|
12
12
|
|
13
|
+
/**
|
14
|
+
* SyncedRoom is a behaviour that will attempt to join a networked room based on the URL parameters or a random room.
|
15
|
+
* It will also create a button in the menu to join or leave the room.
|
16
|
+
* You can also join a networked room by calling the core methods like `this.context.connection.joinRoom("roomName")`.
|
17
|
+
*/
|
13
18
|
export class SyncedRoom extends Behaviour {
|
14
19
|
|
20
|
+
/**
|
21
|
+
* The name of the room to join.
|
22
|
+
*/
|
15
23
|
@serializable()
|
16
24
|
public roomName!: string;
|
25
|
+
/**
|
26
|
+
* The URL parameter name to use for the room name. E.g. if set to "room" the URL will look like `?room=roomName`.
|
27
|
+
*/
|
17
28
|
@serializable()
|
18
29
|
public urlParameterName: string = "room";
|
30
|
+
/**
|
31
|
+
* If true, the room will be joined automatically when this component becomes active
|
32
|
+
*/
|
19
33
|
@serializable()
|
20
34
|
public joinRandomRoom: boolean = true;
|
35
|
+
/**
|
36
|
+
* If true and no room parameter is found in the URL then no room will be joined.
|
37
|
+
*/
|
21
38
|
@serializable()
|
22
39
|
public requireRoomParameter: boolean = false;
|
40
|
+
/**
|
41
|
+
* If true, the room will be rejoined automatically when disconnected.
|
42
|
+
*/
|
23
43
|
@serializable()
|
24
44
|
public autoRejoin: boolean = true;
|
25
45
|
|
46
|
+
/**
|
47
|
+
* If true, a join/leave room button will be created in the menu.
|
48
|
+
*/
|
26
49
|
@serializable()
|
27
50
|
public createJoinButton: boolean = true;
|
28
51
|
|
29
52
|
private _roomPrefix?: string;
|
30
53
|
|
54
|
+
/**
|
55
|
+
* The room prefix to use for the room name. E.g. if set to "room_" and the room name is "name" the final room name will be "room_name".
|
56
|
+
*/
|
57
|
+
public get roomPrefix(): string | undefined {
|
58
|
+
return this._roomPrefix;
|
59
|
+
}
|
60
|
+
/** @deprecated use roomPrefix */
|
31
61
|
public get RoomPrefix(): string | undefined {
|
32
62
|
return this._roomPrefix;
|
33
63
|
}
|
34
64
|
|
65
|
+
/** @internal */
|
35
66
|
awake() {
|
36
67
|
if (debug) console.log("Room", this.roomName, this.urlParameterName, this.joinRandomRoom, this.requireRoomParameter, this.autoRejoin);
|
37
68
|
if (this._roomPrefix === undefined) {
|
@@ -40,6 +71,7 @@
|
|
40
71
|
}
|
41
72
|
}
|
42
73
|
|
74
|
+
/** @internal */
|
43
75
|
onEnable() {
|
44
76
|
// if the url contains a view parameter override room and join in view mode
|
45
77
|
const viewId = utils.getParam(viewParamName);
|
@@ -58,12 +90,14 @@
|
|
58
90
|
}
|
59
91
|
}
|
60
92
|
|
93
|
+
/** @internal */
|
61
94
|
onDisable(): void {
|
62
95
|
this._roomButton?.remove();
|
63
96
|
if (this.roomName && this.roomName.length > 0)
|
64
97
|
this.context.connection.leaveRoom(this.roomName);
|
65
98
|
}
|
66
99
|
|
100
|
+
/** @internal */
|
67
101
|
onDestroy(): void {
|
68
102
|
this.destroyRoomButton();
|
69
103
|
}
|
@@ -125,6 +159,7 @@
|
|
125
159
|
private _lastRoomTime: number = -1;
|
126
160
|
private _userWantsToBeInARoom = false;
|
127
161
|
|
162
|
+
/** @internal */
|
128
163
|
update(): void {
|
129
164
|
if (this.context.connection.isConnected) {
|
130
165
|
if (this.context.time.time - this._lastPingTime > 3) {
|
@@ -211,7 +246,7 @@
|
|
211
246
|
else {
|
212
247
|
if (this.urlParameterName) {
|
213
248
|
if (!getParam(this.urlParameterName)) {
|
214
|
-
if(this._lastJoinedRoom)
|
249
|
+
if (this._lastJoinedRoom)
|
215
250
|
utils.setParamWithoutReload(this.urlParameterName, this._lastJoinedRoom);
|
216
251
|
else
|
217
252
|
this.setRandomRoomUrlParameter();
|
@@ -19,6 +19,8 @@
|
|
19
19
|
|
20
20
|
const builder = new flatbuffers.Builder();
|
21
21
|
|
22
|
+
/** Creates a flatbuffer model containing the transform data of a game object. Used by {@link SyncedTransform}
|
23
|
+
*/
|
22
24
|
export function createTransformModel(guid: string, b: Behaviour, fast: boolean = true): Uint8Array {
|
23
25
|
builder.clear();
|
24
26
|
const guidObj = builder.createString(guid);
|
@@ -47,6 +49,9 @@
|
|
47
49
|
if(debug && FAST_INTERVAL > 0) console.log("Sync Transform Fast Interval", FAST_INTERVAL);
|
48
50
|
})
|
49
51
|
|
52
|
+
/**
|
53
|
+
* SyncedTransform is a behaviour that syncs the transform of a game object over the network.
|
54
|
+
*/
|
50
55
|
export class SyncedTransform extends Behaviour {
|
51
56
|
|
52
57
|
|
@@ -93,6 +98,7 @@
|
|
93
98
|
private joinedRoomCallback: any = null;
|
94
99
|
private receivedDataCallback: any = null;
|
95
100
|
|
101
|
+
/** @internal */
|
96
102
|
awake() {
|
97
103
|
if (debug)
|
98
104
|
console.log("new instance", this.guid, this);
|
@@ -122,6 +128,7 @@
|
|
122
128
|
this.context.connection.beginListenBinary(SyncedTransformIdentifier, this.receivedDataCallback);
|
123
129
|
}
|
124
130
|
|
131
|
+
/** @internal */
|
125
132
|
onDestroy(): void {
|
126
133
|
// TODO: can we add a new component for this?! do we really need this?!
|
127
134
|
if (this.syncDestroy)
|
@@ -174,6 +181,7 @@
|
|
174
181
|
}
|
175
182
|
}
|
176
183
|
|
184
|
+
/** @internal */
|
177
185
|
onEnable(): void {
|
178
186
|
this.lastWorldPos.copy(this.worldPosition);
|
179
187
|
this.lastWorldRotation.copy(this.worldQuaternion);
|
@@ -184,6 +192,7 @@
|
|
184
192
|
}
|
185
193
|
}
|
186
194
|
|
195
|
+
/** @internal */
|
187
196
|
onDisable(): void {
|
188
197
|
if (this._model)
|
189
198
|
this._model.freeOwnership();
|
@@ -194,6 +203,7 @@
|
|
194
203
|
private lastWorldPos!: Vector3;
|
195
204
|
private lastWorldRotation!: Quaternion;
|
196
205
|
|
206
|
+
/** @internal */
|
197
207
|
onBeforeRender() {
|
198
208
|
if (!this.activeAndEnabled || !this.context.connection.isConnected) return;
|
199
209
|
// console.log("BEFORE RENDER", this.destroyed, this.guid, this._model?.isOwned, this.name, this.gameObject);
|
@@ -8,12 +8,15 @@
|
|
8
8
|
import { Rigidbody } from "./RigidBody.js";
|
9
9
|
import { createTransformModel, SyncedTransformIdentifier } from "./SyncedTransform.js";
|
10
10
|
|
11
|
+
/** The TestRunner component is used to run tests when the scene starts
|
12
|
+
* @internal */
|
11
13
|
export class TestRunner extends Behaviour {
|
12
14
|
awake(): void {
|
13
15
|
tests.detect_run_tests();
|
14
16
|
}
|
15
17
|
}
|
16
18
|
|
19
|
+
/** @internal */
|
17
20
|
export class TestSimulateUserData extends Behaviour {
|
18
21
|
|
19
22
|
transformsPerFrame: number = 10;
|
@@ -13,6 +13,10 @@
|
|
13
13
|
|
14
14
|
const debug = getParam("debugtimeline");
|
15
15
|
|
16
|
+
/**
|
17
|
+
* A TrackHandler is responsible for evaluating a specific type of timeline track.
|
18
|
+
* A timeline track can be an animation track, audio track, signal track, control track etc and is controlled by a {@link PlayableDirector}.
|
19
|
+
*/
|
16
20
|
export abstract class TrackHandler {
|
17
21
|
director!: PlayableDirector;
|
18
22
|
track!: Models.TrackModel;
|
@@ -7,6 +7,9 @@
|
|
7
7
|
import { OrbitControls } from "./OrbitControls.js";
|
8
8
|
import { SyncedTransform } from "./SyncedTransform.js";
|
9
9
|
|
10
|
+
/**
|
11
|
+
* TransformGizmo is a component that displays a gizmo for transforming the object in the scene.
|
12
|
+
*/
|
10
13
|
export class TransformGizmo extends Behaviour {
|
11
14
|
|
12
15
|
@serializable()
|
@@ -24,6 +27,7 @@
|
|
24
27
|
private control?: TransformControls;
|
25
28
|
private orbit?: OrbitControls;
|
26
29
|
|
30
|
+
/** @internal */
|
27
31
|
onEnable() {
|
28
32
|
if (this.isGizmo && !params.showGizmos) return;
|
29
33
|
|
@@ -58,6 +62,7 @@
|
|
58
62
|
}
|
59
63
|
}
|
60
64
|
|
65
|
+
/** @internal */
|
61
66
|
onDisable() {
|
62
67
|
this.control?.removeFromParent();
|
63
68
|
this.control?.removeEventListener('dragging-changed', this.onControlChangedEvent);
|
@@ -3,6 +3,7 @@
|
|
3
3
|
import { slerp } from "../../engine/engine_three_utils.js";
|
4
4
|
import { applyPrototypeExtensions, registerPrototypeExtensions } from "./ExtensionUtils.js";
|
5
5
|
|
6
|
+
/** @internal */
|
6
7
|
export function apply(object: Vector3) {
|
7
8
|
if (object && object.isVector3 === true) {
|
8
9
|
applyPrototypeExtensions(object, Vector3);
|
@@ -26,10 +26,14 @@
|
|
26
26
|
private _postprocessing?: PostProcessingHandler;
|
27
27
|
private _effects: PostProcessingEffect[] = [];
|
28
28
|
|
29
|
+
/**
|
30
|
+
* When dirty the post processing effects will be re-applied
|
31
|
+
*/
|
29
32
|
markDirty() {
|
30
33
|
this._isDirty = true;
|
31
34
|
}
|
32
35
|
|
36
|
+
/** @internal */
|
33
37
|
awake() {
|
34
38
|
if (debug) {
|
35
39
|
console.log(this);
|
@@ -47,10 +51,12 @@
|
|
47
51
|
this.sharedProfile?.init();
|
48
52
|
}
|
49
53
|
|
54
|
+
/** @internal */
|
50
55
|
onDisable() {
|
51
56
|
this._postprocessing?.unapply();
|
52
57
|
}
|
53
58
|
|
59
|
+
/** @internal */
|
54
60
|
onBeforeRender(): void {
|
55
61
|
if (!this.context.isInXR) {
|
56
62
|
|
@@ -75,6 +81,7 @@
|
|
75
81
|
}
|
76
82
|
}
|
77
83
|
|
84
|
+
/** @internal */
|
78
85
|
onDestroy(): void {
|
79
86
|
this._postprocessing?.dispose();
|
80
87
|
}
|
@@ -27,6 +27,7 @@
|
|
27
27
|
return PostProcessingEffect;
|
28
28
|
}
|
29
29
|
|
30
|
+
/** @internal */
|
30
31
|
export class VolumeProfile {
|
31
32
|
|
32
33
|
@serializeable([d => resolveComponentType(d), PostProcessingEffect])
|
@@ -18,8 +18,12 @@
|
|
18
18
|
|
19
19
|
const debug = getParam("debugarcamera");
|
20
20
|
|
21
|
+
/**
|
22
|
+
* WebARCameraBackground is a component that allows to display the camera feed as a background in an AR session to more easily blend the real world with the virtual world or applying effects to the camera feed.
|
23
|
+
*/
|
21
24
|
export class WebARCameraBackground extends Behaviour {
|
22
25
|
|
26
|
+
/** @internal */
|
23
27
|
onBeforeXR(_mode: XRSessionMode, args: XRSessionInit): void {
|
24
28
|
args.optionalFeatures = args.optionalFeatures || [];
|
25
29
|
args.optionalFeatures.push('camera-access');
|
@@ -27,6 +31,7 @@
|
|
27
31
|
if (debug) console.warn("Requesting camera-access");
|
28
32
|
}
|
29
33
|
|
34
|
+
/** @internal */
|
30
35
|
onEnterXR(_args: NeedleXREventArgs): void {
|
31
36
|
if (this.backgroundPlane) {
|
32
37
|
this.context.scene.add(this.backgroundPlane);
|
@@ -37,6 +42,7 @@
|
|
37
42
|
this.context.pre_render_callbacks.push(this.preRender);
|
38
43
|
}
|
39
44
|
|
45
|
+
/** @internal */
|
40
46
|
onLeaveXR(_args: NeedleXREventArgs): void {
|
41
47
|
if (this.backgroundPlane) this.backgroundPlane.removeFromParent();
|
42
48
|
const i = this.context.pre_render_callbacks.indexOf(this.preRender);
|
@@ -44,6 +50,9 @@
|
|
44
50
|
this.context.pre_render_callbacks.splice(i, 1);
|
45
51
|
}
|
46
52
|
|
53
|
+
/**
|
54
|
+
* The tint color of the camera feed
|
55
|
+
*/
|
47
56
|
@serializable(RGBAColor)
|
48
57
|
public backgroundTint: RGBAColor = new RGBAColor(1,1,1,1);
|
49
58
|
|
@@ -69,6 +78,7 @@
|
|
69
78
|
|
70
79
|
|
71
80
|
|
81
|
+
/** @internal */
|
72
82
|
private preRender = () => {
|
73
83
|
if (!this || !this.gameObject) return;
|
74
84
|
|
@@ -98,6 +108,7 @@
|
|
98
108
|
}
|
99
109
|
}
|
100
110
|
|
111
|
+
/** @internal */
|
101
112
|
onBeforeRender(frame: XRFrame | null) {
|
102
113
|
this.updateFromFrame(frame);
|
103
114
|
}
|
@@ -174,6 +185,9 @@
|
|
174
185
|
`;
|
175
186
|
|
176
187
|
// not sure where we want to move this and in which form is best (extends Object3D?)
|
188
|
+
/**
|
189
|
+
* Creates a fullscreen plane with a shader that can be used to display a camera feed or other background.
|
190
|
+
*/
|
177
191
|
export function makeFullscreenPlane(tint: RGBAColor ) {
|
178
192
|
const replacementTint = "vec4(" + tint.r.toFixed(3) + "," + tint.g.toFixed(3) + "," + tint.b.toFixed(3) + "," + tint.a.toFixed(3) + ")";
|
179
193
|
if (debug) console.log(replacementTint);
|
@@ -20,6 +20,10 @@
|
|
20
20
|
|
21
21
|
// TODO: webarsessionroot needs to place the rig (and not itself)
|
22
22
|
|
23
|
+
/**
|
24
|
+
* The WebARSessionRoot is the root object for a WebAR session and used to place the scene in AR.
|
25
|
+
* It is also responsible for scaling the user in AR and optionally creating a XR anchor for the scene placement.
|
26
|
+
*/
|
23
27
|
export class WebARSessionRoot extends Behaviour {
|
24
28
|
|
25
29
|
/** The scale of a user in AR:
|
@@ -5,6 +5,7 @@
|
|
5
5
|
import { serializable } from "../../engine/engine_serialization.js";
|
6
6
|
import { getParam, isDesktop, isiOS, isMobileDevice, isQuest, isSafari } from "../../engine/engine_utils.js";
|
7
7
|
import { type NeedleXREventArgs, NeedleXRSession } from "../../engine/engine_xr.js";
|
8
|
+
import { ButtonsFactory } from "../../engine/webcomponents/buttons.js";
|
8
9
|
import { getIconElement } from "../../engine/webcomponents/icons.js";
|
9
10
|
import { PlayerSync } from "../../engine-components-experimental/networking/PlayerSync.js";
|
10
11
|
import { Behaviour, GameObject } from "../Component.js";
|
@@ -20,6 +21,10 @@
|
|
20
21
|
const debug = getParam("debugwebxr");
|
21
22
|
const debugQuicklook = getParam("debugusdz");
|
22
23
|
|
24
|
+
/**
|
25
|
+
* WebXR component to enable VR, AR and Quicklook on iOS in your scene.
|
26
|
+
* It provides a simple wrapper around the {@link NeedleXRSession} API and adds some additional features like creating buttons or enabling default movement behaviour.
|
27
|
+
*/
|
23
28
|
export class WebXR extends Behaviour {
|
24
29
|
|
25
30
|
// UI
|
@@ -305,7 +310,7 @@
|
|
305
310
|
if (this.createQRCode && !isMobileDevice()) {
|
306
311
|
NeedleXRSession.isXRSupported().then(supported => {
|
307
312
|
if (isDesktop() || !supported) {
|
308
|
-
const qrCode =
|
313
|
+
const qrCode = ButtonsFactory.getOrCreate().createQRCode();
|
309
314
|
this.addButton(qrCode, xrButtonsPriority);
|
310
315
|
}
|
311
316
|
});
|
@@ -2,12 +2,19 @@
|
|
2
2
|
import { generateQRCode } from "../../engine/engine_utils.js";
|
3
3
|
import { isMozillaXR } from "../../engine/engine_utils.js";
|
4
4
|
import { NeedleXRSession } from "../../engine/engine_xr.js";
|
5
|
+
import { ButtonsFactory } from "../../engine/webcomponents/buttons.js";
|
5
6
|
import { getIconElement } from "../../engine/webcomponents/icons.js";
|
7
|
+
import { onXRSessionStart } from "../../engine/xr/events.js";
|
6
8
|
import { GameObject } from "../Component.js";
|
7
9
|
import { USDZExporter } from "../export/usdz/USDZExporter.js";
|
8
10
|
|
9
11
|
// TODO: move these buttons into their own web components so their logic is encapsulated (e.g. the CSS animation when a xr session is requested)
|
10
12
|
|
13
|
+
/**
|
14
|
+
* Factory to create WebXR buttons for AR, VR, Quicklook and Send to Quest
|
15
|
+
* The buttons are created as HTMLButtonElements and can be added to the DOM.
|
16
|
+
* The buttons will automatically hide when a XR session is started and show again when the session ends.
|
17
|
+
*/
|
11
18
|
export class WebXRButtonFactory {
|
12
19
|
|
13
20
|
private static _instance: WebXRButtonFactory;
|
@@ -37,10 +44,8 @@
|
|
37
44
|
get sendToQuestButton() { return this._sendToQuestButton; }
|
38
45
|
private _sendToQuestButton?: HTMLButtonElement;
|
39
46
|
|
40
|
-
get qrButton() { return
|
41
|
-
private _qrButton?: HTMLButtonElement;
|
47
|
+
get qrButton() { return ButtonsFactory.getOrCreate().createQRCode(); }
|
42
48
|
|
43
|
-
|
44
49
|
/** get or create the quicklook button
|
45
50
|
* Behaviour of the button:
|
46
51
|
* - if the button is clicked a USDZExporter component will be searched for in the scene and if found, it will be used to export the scene to USDZ / Quicklook
|
@@ -173,96 +178,11 @@
|
|
173
178
|
return button;
|
174
179
|
}
|
175
180
|
|
181
|
+
/**
|
182
|
+
* @deprecated please use ButtonsFactory.getOrCreate().createQRCode(). This method will be removed in a future update
|
183
|
+
*/
|
176
184
|
createQRCode(): HTMLButtonElement {
|
177
|
-
|
178
|
-
|
179
|
-
const qrCodeButton = document.createElement("button");
|
180
|
-
this._qrButton = qrCodeButton;
|
181
|
-
qrCodeButton.innerText = "QR Code";
|
182
|
-
qrCodeButton.prepend(getIconElement("qr_code"));
|
183
|
-
qrCodeButton.title = "Scan this QR code with your phone to open this page";
|
184
|
-
this.hideElementDuringXRSession(qrCodeButton);
|
185
|
-
|
186
|
-
|
187
|
-
const qrCodeContainer = document.createElement("div");
|
188
|
-
qrCodeContainer.style.position = "fixed";
|
189
|
-
qrCodeContainer.style.display = "inline-block";
|
190
|
-
qrCodeContainer.style.padding = "1rem";
|
191
|
-
qrCodeContainer.style.backgroundColor = "white";
|
192
|
-
qrCodeContainer.style.borderRadius = "0.4rem";
|
193
|
-
qrCodeContainer.style.cursor = "pointer";
|
194
|
-
qrCodeContainer.style.zIndex = "1000";
|
195
|
-
qrCodeContainer.style.boxShadow = "0 0 12px rgba(0, 0, 0, 0.2)";
|
196
|
-
|
197
|
-
const qrCodeElement = document.createElement("div");
|
198
|
-
qrCodeElement.classList.add("qr-code-container");
|
199
|
-
qrCodeContainer.appendChild(qrCodeElement);
|
200
|
-
|
201
|
-
qrCodeButton.addEventListener("click", () => {
|
202
|
-
if (qrCodeContainer.parentNode) return hideQRCode();
|
203
|
-
showQRCode();
|
204
|
-
});
|
205
|
-
|
206
|
-
/** shows the QRCode near the button */
|
207
|
-
async function showQRCode() {
|
208
|
-
// generate the qr code when the button is clicked
|
209
|
-
// this ensures that we get the QRcode with the latest URL
|
210
|
-
await generateAndInsertQRCode();
|
211
|
-
// TODO: make sure it doesnt overflow the screen
|
212
|
-
// we need to add the qrCodeContainer to the body to get the correct size
|
213
|
-
document.body.appendChild(qrCodeContainer);
|
214
|
-
const containerRect = qrCodeElement.getBoundingClientRect();
|
215
|
-
const buttonRect = qrCodeButton.getBoundingClientRect();
|
216
|
-
qrCodeContainer.style.left = (buttonRect.left + buttonRect.width * .5 - containerRect.width * .5) + "px";
|
217
|
-
const isButtonInTopHalf = buttonRect.top < containerRect.height;
|
218
|
-
if (isButtonInTopHalf)
|
219
|
-
qrCodeContainer.style.top = `calc(${buttonRect.bottom}px + ${qrCodeContainer.style.padding} * .6)`;
|
220
|
-
else
|
221
|
-
qrCodeContainer.style.top = `calc(${buttonRect.top - containerRect.height}px - ${qrCodeContainer.style.padding} * 2.5)`;
|
222
|
-
qrCodeContainer.style.opacity = "0";
|
223
|
-
qrCodeContainer.style.pointerEvents = "all";
|
224
|
-
qrCodeContainer.style.transition = "opacity 0.2s ease-in-out";
|
225
|
-
|
226
|
-
// context click to hide the QR code again, if we dont timeout the event will be triggered immediately
|
227
|
-
setTimeout(() => {
|
228
|
-
qrCodeContainer.style.opacity = "1";
|
229
|
-
window.addEventListener("click", hideQRCode, { once: true })
|
230
|
-
});
|
231
|
-
window.addEventListener("resize", hideQRCode);
|
232
|
-
window.addEventListener("scroll", hideQRCode);
|
233
|
-
|
234
|
-
document.body.appendChild(qrCodeContainer);
|
235
|
-
}
|
236
|
-
|
237
|
-
/** hides to QRCode overlay and unsubscribes from events */
|
238
|
-
function hideQRCode() {
|
239
|
-
qrCodeContainer.style.pointerEvents = "none";
|
240
|
-
qrCodeContainer.style.transition = "opacity 0.2s";
|
241
|
-
qrCodeContainer.style.opacity = "0";
|
242
|
-
setTimeout(() => qrCodeContainer.parentNode?.removeChild(qrCodeContainer), 500);
|
243
|
-
window.removeEventListener("click", hideQRCode);
|
244
|
-
window.removeEventListener("resize", hideQRCode);
|
245
|
-
window.removeEventListener("scroll", hideQRCode);
|
246
|
-
};
|
247
|
-
|
248
|
-
/** generates a QR code and inserts it into the qrCodeElement */
|
249
|
-
async function generateAndInsertQRCode() {
|
250
|
-
const size = 200;
|
251
|
-
const code = await generateQRCode({
|
252
|
-
text: window.location.href,
|
253
|
-
width: size,
|
254
|
-
height: size,
|
255
|
-
});
|
256
|
-
qrCodeElement.innerHTML = "";
|
257
|
-
qrCodeElement.appendChild(code);
|
258
|
-
}
|
259
|
-
|
260
|
-
// lazily create the qr button
|
261
|
-
qrCodeButton.addEventListener("pointerenter", () => {
|
262
|
-
generateAndInsertQRCode();
|
263
|
-
}, { once: true });
|
264
|
-
|
265
|
-
return qrCodeButton;
|
185
|
+
return ButtonsFactory.getOrCreate().createQRCode();
|
266
186
|
}
|
267
187
|
|
268
188
|
private updateSessionSupported(button: HTMLButtonElement, mode: XRSessionMode) {
|
@@ -277,6 +197,9 @@
|
|
277
197
|
}
|
278
198
|
|
279
199
|
private hideElementDuringXRSession(element: HTMLElement) {
|
200
|
+
onXRSessionStart(_ => {
|
201
|
+
})
|
202
|
+
onXRSessionStart
|
280
203
|
NeedleXRSession.onXRSessionStart(_ => {
|
281
204
|
element["previous-display"] = element.style.display;
|
282
205
|
element.style.display = "none";
|
@@ -10,6 +10,10 @@
|
|
10
10
|
|
11
11
|
const debug = getParam("debugwebxr");
|
12
12
|
|
13
|
+
/**
|
14
|
+
* A user in XR (VR or AR) is parented to an XR rig during the session.
|
15
|
+
* When moving through the scene the rig is moved instead of the user.
|
16
|
+
*/
|
13
17
|
export class XRRig extends Behaviour implements IXRRig {
|
14
18
|
|
15
19
|
@serializable()
|
@@ -21,6 +21,9 @@
|
|
21
21
|
const handsJointBuffer = new Float32Array(16 * 25);
|
22
22
|
const renderingUpdateTimings = new Array<number>();
|
23
23
|
|
24
|
+
/**
|
25
|
+
* XRControllerModel is a component that allows to display controller models or hand models in an XR session.
|
26
|
+
*/
|
24
27
|
export class XRControllerModel extends Behaviour {
|
25
28
|
|
26
29
|
@serializable()
|
@@ -17,6 +17,9 @@
|
|
17
17
|
|
18
18
|
const debug = getParam("debugwebxr");
|
19
19
|
|
20
|
+
/**
|
21
|
+
* XRControllerMovement is a component that allows to move the XR rig using the XR controller input.
|
22
|
+
*/
|
20
23
|
export class XRControllerMovement extends Behaviour implements XRMovementBehaviour {
|
21
24
|
|
22
25
|
/** Movement speed in meters per second */
|