@needle-tools/engine 4.12.1 → 4.12.3-next.a27c12e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -1
- package/dist/{needle-engine.bundle-BPNuWGjI.min.js → needle-engine.bundle-BHCgy9h1.min.js} +138 -139
- package/dist/{needle-engine.bundle-BV9DUnnS.umd.cjs → needle-engine.bundle-CLfNhYon.umd.cjs} +127 -128
- package/dist/{needle-engine.bundle-C4PyE3dQ.js → needle-engine.bundle-URbCchz5.js} +3531 -3470
- package/dist/needle-engine.d.ts +15 -5
- package/dist/needle-engine.js +2 -2
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/lib/asap/needle-asap.js +1 -1
- package/lib/asap/needle-asap.js.map +1 -1
- package/lib/engine/debug/debug_overlay.js +6 -2
- package/lib/engine/debug/debug_overlay.js.map +1 -1
- package/lib/engine/engine_components.d.ts +2 -1
- package/lib/engine/engine_components.js +4 -3
- package/lib/engine/engine_components.js.map +1 -1
- package/lib/engine/engine_input.d.ts +4 -1
- package/lib/engine/engine_input.js +24 -20
- package/lib/engine/engine_input.js.map +1 -1
- package/lib/engine/js-extensions/Object3D.d.ts +6 -2
- package/lib/engine/js-extensions/Object3D.js +6 -4
- package/lib/engine/js-extensions/Object3D.js.map +1 -1
- package/lib/engine/webcomponents/buttons.d.ts +3 -1
- package/lib/engine/webcomponents/buttons.js +2 -2
- package/lib/engine/webcomponents/buttons.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +13 -11
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
- package/lib/engine/webcomponents/needle-button.js +21 -16
- package/lib/engine/webcomponents/needle-button.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.ar-overlay.js +7 -5
- package/lib/engine/webcomponents/needle-engine.ar-overlay.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.js +1 -1
- package/lib/engine/webcomponents/needle-engine.loading.js +1 -1
- package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
- package/lib/engine/xr/NeedleXRSession.js +20 -0
- package/lib/engine/xr/NeedleXRSession.js.map +1 -1
- package/lib/engine/xr/TempXRContext.d.ts +7 -2
- package/lib/engine/xr/TempXRContext.js +143 -44
- package/lib/engine/xr/TempXRContext.js.map +1 -1
- package/lib/engine-components/SpectatorCamera.js +3 -2
- package/lib/engine-components/SpectatorCamera.js.map +1 -1
- package/lib/engine-components/ui/InputField.js +5 -2
- package/lib/engine-components/ui/InputField.js.map +1 -1
- package/package.json +2 -2
- package/src/asap/needle-asap.ts +1 -1
- package/src/engine/debug/debug_overlay.ts +6 -2
- package/src/engine/engine_components.ts +5 -4
- package/src/engine/engine_input.ts +26 -19
- package/src/engine/js-extensions/Object3D.ts +14 -7
- package/src/engine/webcomponents/buttons.ts +2 -2
- package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +15 -13
- package/src/engine/webcomponents/needle-button.ts +23 -19
- package/src/engine/webcomponents/needle-engine.ar-overlay.ts +10 -9
- package/src/engine/webcomponents/needle-engine.loading.ts +1 -1
- package/src/engine/webcomponents/needle-engine.ts +1 -1
- package/src/engine/xr/NeedleXRSession.ts +22 -0
- package/src/engine/xr/TempXRContext.ts +155 -47
- package/src/engine-components/SpectatorCamera.ts +2 -1
- package/src/engine-components/ui/InputField.ts +11 -6
|
@@ -22,6 +22,8 @@ import { markHierarchyDirty } from "../engine_mainloop_utils.js";
|
|
|
22
22
|
import { applyPrototypeExtensions, registerPrototypeExtensions } from "./ExtensionUtils.js";
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
// #region Type Declarations
|
|
26
|
+
|
|
25
27
|
// NOTE: keep in sync with method declarations below
|
|
26
28
|
declare module 'three' {
|
|
27
29
|
export interface Object3D {
|
|
@@ -95,9 +97,11 @@ declare module 'three' {
|
|
|
95
97
|
getComponents<T extends IComponent>(type: Constructor<T>, arr?: []): T[];
|
|
96
98
|
/**
|
|
97
99
|
* Get a Needle Engine component from the {@link Object3D} or its children. This will search on the current Object and all its children.
|
|
100
|
+
* @param type The type of the component to search for.
|
|
101
|
+
* @param includeInactive If true, also inactive components are considered. Default is false.
|
|
98
102
|
* @returns The component instance or null if not found.
|
|
99
103
|
*/
|
|
100
|
-
getComponentInChildren<T extends IComponent>(type: Constructor<T
|
|
104
|
+
getComponentInChildren<T extends IComponent>(type: Constructor<T>, includeInactive?: boolean): T | null;
|
|
101
105
|
/**
|
|
102
106
|
* Get all components of a specific type from the {@link Object3D} or its children. This will search on the current Object and all its children.
|
|
103
107
|
* @param arr Optional array to fill with the found components.
|
|
@@ -106,9 +110,11 @@ declare module 'three' {
|
|
|
106
110
|
getComponentsInChildren<T extends IComponent>(type: Constructor<T>, arr?: []): T[];
|
|
107
111
|
/**
|
|
108
112
|
* Get a Needle Engine component from the {@link Object3D} or its parents. This will search on the current Object and all its parents.
|
|
113
|
+
* @param type The type of the component to search for.
|
|
114
|
+
* @param includeInactive If true, also inactive components are considered. Default is false.
|
|
109
115
|
* @returns The component instance or null if not found.
|
|
110
116
|
*/
|
|
111
|
-
getComponentInParent<T extends IComponent>(type: Constructor<T
|
|
117
|
+
getComponentInParent<T extends IComponent>(type: Constructor<T>, includeInactive?: boolean): T | null;
|
|
112
118
|
/**
|
|
113
119
|
* Get all Needle Engine components of a specific type from the {@link Object3D} or its parents. This will search on the current Object and all its parents.
|
|
114
120
|
* @param arr Optional array to fill with the found components.
|
|
@@ -200,6 +206,7 @@ if (NEEDLE_ENGINE_FEATURE_FLAGS.experimentalSmartHierarchyUpdate) {
|
|
|
200
206
|
}
|
|
201
207
|
}
|
|
202
208
|
|
|
209
|
+
// #region Prototype Method Implementations
|
|
203
210
|
|
|
204
211
|
Object3D.prototype["SetActive"] = function (active: boolean) {
|
|
205
212
|
this.visible = active;
|
|
@@ -213,7 +220,6 @@ Object3D.prototype["destroy"] = function () {
|
|
|
213
220
|
destroy(this);
|
|
214
221
|
}
|
|
215
222
|
|
|
216
|
-
|
|
217
223
|
Object3D.prototype["addComponent"] = function <T extends IComponent>(comp: T | ConstructorConcrete<T>, init?: ComponentInit<T>) {
|
|
218
224
|
return addComponent(this, comp, init);
|
|
219
225
|
}
|
|
@@ -238,22 +244,23 @@ Object3D.prototype["getComponents"] = function <T extends IComponent>(type: Cons
|
|
|
238
244
|
return getComponents(this, type, arr);
|
|
239
245
|
}
|
|
240
246
|
|
|
241
|
-
Object3D.prototype["getComponentInChildren"] = function <T extends IComponent>(type: Constructor<T
|
|
242
|
-
return getComponentInChildren(this, type);
|
|
247
|
+
Object3D.prototype["getComponentInChildren"] = function <T extends IComponent>(type: Constructor<T>, includeInactive: boolean = false) {
|
|
248
|
+
return getComponentInChildren(this, type, includeInactive);
|
|
243
249
|
}
|
|
244
250
|
|
|
245
251
|
Object3D.prototype["getComponentsInChildren"] = function <T extends IComponent>(type: Constructor<T>, arr?: []) {
|
|
246
252
|
return getComponentsInChildren(this, type, arr);
|
|
247
253
|
}
|
|
248
254
|
|
|
249
|
-
Object3D.prototype["getComponentInParent"] = function <T extends IComponent>(type: Constructor<T
|
|
250
|
-
return getComponentInParent(this, type);
|
|
255
|
+
Object3D.prototype["getComponentInParent"] = function <T extends IComponent>(type: Constructor<T>, includeInactive: boolean = false) {
|
|
256
|
+
return getComponentInParent(this, type, includeInactive);
|
|
251
257
|
}
|
|
252
258
|
|
|
253
259
|
Object3D.prototype["getComponentsInParent"] = function <T extends IComponent>(type: Constructor<T>, arr?: []) {
|
|
254
260
|
return getComponentsInParent(this, type, arr);
|
|
255
261
|
}
|
|
256
262
|
|
|
263
|
+
// #region Prototype Property Implementations
|
|
257
264
|
|
|
258
265
|
// this is a fix to allow gameObject active animation be applied to a three object
|
|
259
266
|
if (!Object.getOwnPropertyDescriptor(Object3D.prototype, "activeSelf")) {
|
|
@@ -176,7 +176,7 @@ export class ButtonsFactory {
|
|
|
176
176
|
* The QR code will be generated with the current URL when the button is clicked
|
|
177
177
|
* @returns the QR code button element
|
|
178
178
|
*/
|
|
179
|
-
createQRCode(): HTMLButtonElement {
|
|
179
|
+
createQRCode(opts?: { anchorElement?: HTMLElement }): HTMLButtonElement {
|
|
180
180
|
|
|
181
181
|
if (this._qrButton) return this._qrButton;
|
|
182
182
|
|
|
@@ -223,7 +223,7 @@ export class ButtonsFactory {
|
|
|
223
223
|
// TODO: we would need to search for the right engine element to insert this into if there are more
|
|
224
224
|
// Insert the QR code overlay inside the needle-engine element
|
|
225
225
|
const engine_element = document.body.querySelector("needle-engine");
|
|
226
|
-
const parent = engine_element || document.body;
|
|
226
|
+
const parent = opts?.anchorElement?.parentElement || engine_element || document.body;
|
|
227
227
|
parent.appendChild(qrCodeContainer);
|
|
228
228
|
const containerRect = qrCodeElement.getBoundingClientRect();
|
|
229
229
|
const buttonRect = qrCodeButton.getBoundingClientRect();
|
|
@@ -8,7 +8,7 @@ import type { Context } from "../../engine_setup.js";
|
|
|
8
8
|
import { lookAtObject } from "../../engine_three_utils.js";
|
|
9
9
|
import { IComponent, IContext, IGameObject } from "../../engine_types.js";
|
|
10
10
|
import { TypeStore } from "../../engine_typestore.js";
|
|
11
|
-
import { DeviceUtilities,getParam } from "../../engine_utils.js";
|
|
11
|
+
import { DeviceUtilities, getParam } from "../../engine_utils.js";
|
|
12
12
|
import { getIconTexture, isIconElement } from "../icons.js";
|
|
13
13
|
|
|
14
14
|
const debug = getParam("debugspatialmenu");
|
|
@@ -303,17 +303,19 @@ export class NeedleSpatialMenu {
|
|
|
303
303
|
// this._context.scene.add(logoObject);
|
|
304
304
|
const textureLoader = new TextureLoader();
|
|
305
305
|
textureLoader.load("./include/needle/poweredbyneedle.webp", (texture) => {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
306
|
+
if (texture) {
|
|
307
|
+
onClick.allowModifyUI = false;
|
|
308
|
+
firstLabel.removeFromParent();
|
|
309
|
+
secondLabel.removeFromParent();
|
|
310
|
+
const aspect = texture.image.width / texture.image.height;
|
|
311
|
+
this._poweredByNeedleElement?.set({
|
|
312
|
+
backgroundImage: texture,
|
|
313
|
+
backgroundOpacity: 1,
|
|
314
|
+
width: .1 * aspect,
|
|
315
|
+
height: .1
|
|
316
|
+
});
|
|
317
|
+
this.markDirty();
|
|
318
|
+
}
|
|
317
319
|
});
|
|
318
320
|
|
|
319
321
|
}
|
|
@@ -342,7 +344,7 @@ export class NeedleSpatialMenu {
|
|
|
342
344
|
if (!fontFamily) {
|
|
343
345
|
fontFamily = ThreeMeshUI.FontLibrary.addFontFamily(this.familyName);
|
|
344
346
|
const normal = fontFamily.addVariant("normal", "normal", "./include/needle/arial-msdf.json", "./include/needle/arial.png") as any as ThreeMeshUI.FontVariant;
|
|
345
|
-
/** @ts-ignore */
|
|
347
|
+
/** @ts-ignore */
|
|
346
348
|
normal?.addEventListener('ready', () => {
|
|
347
349
|
this.markDirty();
|
|
348
350
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isDevEnvironment } from "../debug/index.js";
|
|
2
|
+
import { ButtonsFactory } from "./buttons.js";
|
|
2
3
|
import { iconFontUrl, loadFont } from "./fonts.js";
|
|
3
4
|
import { WebXRButtonFactory } from "./WebXRButtons.js";
|
|
4
5
|
|
|
@@ -41,7 +42,7 @@ const isDev = isDevEnvironment();
|
|
|
41
42
|
*/
|
|
42
43
|
export class NeedleButtonElement extends HTMLElement {
|
|
43
44
|
|
|
44
|
-
static observedAttributes = ["ar", "vr", "quicklook"];
|
|
45
|
+
static observedAttributes = ["ar", "vr", "quicklook", "qrcode"];
|
|
45
46
|
|
|
46
47
|
constructor() {
|
|
47
48
|
super();
|
|
@@ -62,13 +63,13 @@ export class NeedleButtonElement extends HTMLElement {
|
|
|
62
63
|
#button: HTMLButtonElement | undefined;
|
|
63
64
|
/** If AR or VR is requested we create and use the webxr button factory to create a button with default behaviour */
|
|
64
65
|
#webxrfactory: WebXRButtonFactory | undefined;
|
|
66
|
+
#buttonfactory: ButtonsFactory | undefined;
|
|
65
67
|
|
|
66
68
|
#observer: MutationObserver | undefined;
|
|
67
69
|
|
|
68
70
|
#update() {
|
|
69
71
|
this.#button?.remove();
|
|
70
72
|
|
|
71
|
-
|
|
72
73
|
if (this.getAttribute("ar") != null) {
|
|
73
74
|
this.#webxrfactory ??= new WebXRButtonFactory()
|
|
74
75
|
this.#button = this.#webxrfactory.createARButton();
|
|
@@ -81,11 +82,15 @@ export class NeedleButtonElement extends HTMLElement {
|
|
|
81
82
|
this.#webxrfactory ??= new WebXRButtonFactory()
|
|
82
83
|
this.#button = this.#webxrfactory.createQuicklookButton();
|
|
83
84
|
}
|
|
85
|
+
else if (this.getAttribute("qrcode") != null) {
|
|
86
|
+
this.#buttonfactory ??= new ButtonsFactory();
|
|
87
|
+
this.#button = this.#buttonfactory.createQRCode({ anchorElement: this });
|
|
88
|
+
}
|
|
84
89
|
else {
|
|
85
90
|
if (isDev) {
|
|
86
91
|
console.warn("No button type specified for <needle-button>. Use either ar, vr or quicklook attribute.")
|
|
87
92
|
}
|
|
88
|
-
else
|
|
93
|
+
else {
|
|
89
94
|
console.debug("No button type specified for <needle-button>. Use either ar, vr or quicklook attribute.")
|
|
90
95
|
}
|
|
91
96
|
return;
|
|
@@ -96,30 +101,29 @@ export class NeedleButtonElement extends HTMLElement {
|
|
|
96
101
|
this.#styles ??= document.createElement("style");
|
|
97
102
|
this.#styles.innerHTML = `
|
|
98
103
|
button {
|
|
99
|
-
all:
|
|
100
|
-
cursor: inherit;
|
|
101
|
-
color: inherit;
|
|
102
|
-
font-family: inherit;
|
|
103
|
-
gap: inherit;
|
|
104
|
-
white-space: nowrap;
|
|
104
|
+
all: unset;
|
|
105
105
|
}
|
|
106
106
|
`;
|
|
107
107
|
const hasUnstyledAttribute = this.getAttribute("unstyled") != undefined;
|
|
108
108
|
if (!hasUnstyledAttribute) {
|
|
109
109
|
this.#styles.innerHTML += `
|
|
110
110
|
:host {
|
|
111
|
-
display: inline-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
display: inline-flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
justify-content: center;
|
|
114
114
|
width: fit-content;
|
|
115
|
-
transition: background .2s;
|
|
116
115
|
|
|
117
|
-
cursor: pointer;
|
|
118
116
|
padding: 0.4rem .5rem;
|
|
119
|
-
border-radius:
|
|
120
|
-
|
|
117
|
+
border-radius: 100vw;
|
|
118
|
+
|
|
121
119
|
background: rgba(245, 245, 245, .8);
|
|
120
|
+
backdrop-filter: blur(10px);
|
|
121
|
+
|
|
122
|
+
cursor: pointer;
|
|
123
|
+
color: black;
|
|
122
124
|
outline: rgba(0,0,0,.05) 1px solid;
|
|
125
|
+
|
|
126
|
+
transition: all .2s;
|
|
123
127
|
}
|
|
124
128
|
:host(:hover) {
|
|
125
129
|
background: rgba(255, 255, 255, 1);
|
|
@@ -152,8 +156,8 @@ export class NeedleButtonElement extends HTMLElement {
|
|
|
152
156
|
this.#observer?.disconnect();
|
|
153
157
|
this.#observer ??= new MutationObserver(() => this.#updateVisibility());
|
|
154
158
|
this.#observer.observe(this.#button, { attributes: true });
|
|
155
|
-
if(isDev) {
|
|
156
|
-
console.log("Needle Button updated")
|
|
159
|
+
if (isDev) {
|
|
160
|
+
console.log("Needle Button updated", this);
|
|
157
161
|
}
|
|
158
162
|
}
|
|
159
163
|
|
|
@@ -170,7 +174,7 @@ export class NeedleButtonElement extends HTMLElement {
|
|
|
170
174
|
|
|
171
175
|
#onclick = (_ev: MouseEvent) => {
|
|
172
176
|
if (isDev) {
|
|
173
|
-
console.log("Needle Button clicked")
|
|
177
|
+
console.log("Needle Button clicked", { defaultPrevented: _ev.defaultPrevented, hasButton: !!this.#button });
|
|
174
178
|
}
|
|
175
179
|
if (_ev.defaultPrevented) return;
|
|
176
180
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Context } from "../engine_setup.js";
|
|
2
|
-
import { DeviceUtilities,getParam } from "../engine_utils.js";
|
|
2
|
+
import { DeviceUtilities, getParam } from "../engine_utils.js";
|
|
3
3
|
|
|
4
4
|
const debug = getParam("debugoverlay");
|
|
5
5
|
export const arContainerClassName = "ar";
|
|
@@ -28,7 +28,7 @@ export class AROverlayHandler {
|
|
|
28
28
|
this.currentSession = session;
|
|
29
29
|
this.arContainer = overlayContainer;
|
|
30
30
|
|
|
31
|
-
if (DeviceUtilities.isMozillaXR()) {
|
|
31
|
+
if (DeviceUtilities.isMozillaXR() || DeviceUtilities.isNeedleAppClip()) {
|
|
32
32
|
const arElements = context.domElement!.children;
|
|
33
33
|
for (let i = 0; i < arElements?.length; i++) {
|
|
34
34
|
const el = arElements[i];
|
|
@@ -38,10 +38,9 @@ export class AROverlayHandler {
|
|
|
38
38
|
this.arContainer?.appendChild(el);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
if(overlayContainer) {
|
|
41
|
+
if (overlayContainer) {
|
|
42
42
|
this.originalDomOverlayParent = overlayContainer.parentNode;
|
|
43
|
-
if (this.originalDomOverlayParent)
|
|
44
|
-
{
|
|
43
|
+
if (this.originalDomOverlayParent) {
|
|
45
44
|
console.log("Reparent DOM Overlay to body", overlayContainer, overlayContainer.style.display);
|
|
46
45
|
// mozilla webxr does hide elements on session start
|
|
47
46
|
// this is only necessary if we generated the overlay element
|
|
@@ -119,18 +118,20 @@ export class AROverlayHandler {
|
|
|
119
118
|
|
|
120
119
|
private ensureQuitARButton(element: HTMLElement) {
|
|
121
120
|
|
|
122
|
-
// No quit button in app clips, we provide one via the native UI
|
|
123
|
-
if (DeviceUtilities.isNeedleAppClip()) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
121
|
|
|
127
122
|
const quitARSlot = document.createElement("slot");
|
|
123
|
+
quitARSlot.style.display = "contents";
|
|
128
124
|
quitARSlot.setAttribute("name", "quit-ar");
|
|
129
125
|
this.appendElement(quitARSlot, element);
|
|
130
126
|
this._createdAROnlyElements.push(quitARSlot);
|
|
131
127
|
// for mozilla XR reparenting we have to make sure the close button is clickable so we set it on the element directly
|
|
132
128
|
// it's in general perhaps more safe to set it on the element to ensure it's clickable
|
|
133
129
|
quitARSlot.style.pointerEvents = "auto";
|
|
130
|
+
// No default quit button in the top right corner in app clips
|
|
131
|
+
// we provide one via the native UI
|
|
132
|
+
if (DeviceUtilities.isNeedleAppClip()) {
|
|
133
|
+
quitARSlot.style.display = "none";
|
|
134
|
+
}
|
|
134
135
|
|
|
135
136
|
// We want to search the document if there's a quit-ar button
|
|
136
137
|
// In which case we don't want to populate the default button (slot) with any content
|
|
@@ -288,7 +288,7 @@ export class EngineLoadingView implements ILoadingViewHandler {
|
|
|
288
288
|
logo.src = needleLogoOnlySVG;
|
|
289
289
|
let isUsingCustomLogo = false;
|
|
290
290
|
if (hasLicense && this._element) {
|
|
291
|
-
const customLogo = this._element.getAttribute("
|
|
291
|
+
const customLogo = this._element.getAttribute("logo-src");
|
|
292
292
|
if (customLogo) {
|
|
293
293
|
isUsingCustomLogo = true;
|
|
294
294
|
logo.src = customLogo;
|
|
@@ -254,7 +254,7 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
|
|
|
254
254
|
<canvas></canvas>
|
|
255
255
|
</div>
|
|
256
256
|
<div class="content">
|
|
257
|
-
<slot class="overlay-content"></slot>
|
|
257
|
+
<slot class="overlay-content" style="display: contents;"></slot>
|
|
258
258
|
</div>
|
|
259
259
|
`;
|
|
260
260
|
// #endregion
|
|
@@ -1111,6 +1111,28 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
1111
1111
|
else if (debug) {
|
|
1112
1112
|
console.warn("controllerAutoUpdate is not available in three.js - cannot disable it");
|
|
1113
1113
|
}
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
// In the appclip we want to make sure the canvas resolution is correctly applied with the DPR
|
|
1117
|
+
// this change has been added to Needle Go AppClip in daa9c186e49fe9d6f14d200e307eb0664e35ad72
|
|
1118
|
+
// But for an immediate fix for Needle Engine projects we also add it here.
|
|
1119
|
+
// We can most likely remove the folllowing block in march 2026
|
|
1120
|
+
if (DeviceUtilities.isNeedleAppClip()) {
|
|
1121
|
+
window.requestAnimationFrame(() => {
|
|
1122
|
+
const canvas = this.context.renderer.domElement;
|
|
1123
|
+
const dpr = window.devicePixelRatio || 1;
|
|
1124
|
+
const currentWidth = canvas.width;
|
|
1125
|
+
const currentHeight = canvas.height;
|
|
1126
|
+
// Check if DPR is already applied (avoid double-scaling)
|
|
1127
|
+
const expectedWidth = Math.floor(window.innerWidth * dpr);
|
|
1128
|
+
const expectedHeight = Math.floor(window.innerHeight * dpr);
|
|
1129
|
+
if (Math.abs(currentWidth - expectedWidth) > 2 || Math.abs(currentHeight - expectedHeight) > 2) {
|
|
1130
|
+
canvas.width = expectedWidth;
|
|
1131
|
+
canvas.height = expectedHeight;
|
|
1132
|
+
console.debug("Applied DPR scaling for Needle AppClip XR session", dpr, canvas.width, canvas.height);
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1114
1136
|
}
|
|
1115
1137
|
|
|
1116
1138
|
/** called when renderer.setSession is fulfilled */
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { AxesHelper, Camera, Color, DirectionalLight, Fog, GridHelper, Mesh, MeshBasicMaterial, MeshStandardMaterial, PerspectiveCamera, PointLight, Scene, WebGLRenderer } from "three";
|
|
1
|
+
import { ArrayCamera, AxesHelper, Camera, Color, DirectionalLight, Fog, GridHelper, Mesh, MeshBasicMaterial, MeshStandardMaterial, PerspectiveCamera, PlaneGeometry, PointLight, Scene, TextureLoader, Vector3, WebGLRenderer } from "three";
|
|
2
2
|
|
|
3
|
+
import { needleLogoOnlySVG } from "../assets/index.js";
|
|
4
|
+
import { isDevEnvironment } from "../debug/index.js";
|
|
3
5
|
import { ObjectUtils, PrimitiveType } from "../engine_create_objects.js";
|
|
6
|
+
import { hasCommercialLicense } from "../engine_license.js";
|
|
4
7
|
import { Mathf } from "../engine_math.js";
|
|
5
|
-
import { delay } from "../engine_utils.js";
|
|
8
|
+
import { delay, DeviceUtilities } from "../engine_utils.js";
|
|
6
9
|
|
|
7
10
|
export declare type SessionInfo = { session: XRSession, mode: XRSessionMode, init: XRSessionInit };
|
|
8
11
|
|
|
@@ -77,6 +80,9 @@ export class TemporaryXRContext {
|
|
|
77
80
|
get isAR() {
|
|
78
81
|
return this._mode === "immersive-ar";
|
|
79
82
|
}
|
|
83
|
+
get isVR() {
|
|
84
|
+
return this._mode === "immersive-vr";
|
|
85
|
+
}
|
|
80
86
|
|
|
81
87
|
private readonly _renderer: WebGLRenderer;
|
|
82
88
|
private readonly _camera: Camera;
|
|
@@ -88,19 +94,31 @@ export class TemporaryXRContext {
|
|
|
88
94
|
this._session = session;
|
|
89
95
|
this._session.addEventListener("end", this.onEnd);
|
|
90
96
|
|
|
91
|
-
this._renderer = new WebGLRenderer({ alpha: true, antialias: true });
|
|
97
|
+
this._renderer = new WebGLRenderer({ alpha: true, antialias: true });
|
|
92
98
|
this._renderer.outputColorSpace = 'srgb';
|
|
99
|
+
// Set pixel ratio and size
|
|
100
|
+
this._renderer.setPixelRatio(Math.min(2, window.devicePixelRatio));
|
|
101
|
+
this._renderer.setSize(window.innerWidth, window.innerHeight, true);
|
|
102
|
+
if (DeviceUtilities.isNeedleAppClip()) {
|
|
103
|
+
window.requestAnimationFrame(() => {
|
|
104
|
+
const dpr = Math.min(2, window.devicePixelRatio);
|
|
105
|
+
const expectedWidth = Math.floor(window.innerWidth * dpr);
|
|
106
|
+
const expectedHeight = Math.floor(window.innerHeight * dpr);
|
|
107
|
+
this._renderer.domElement.width = expectedWidth;
|
|
108
|
+
this._renderer.domElement.height = expectedHeight;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
93
112
|
this._renderer.setAnimationLoop(this.onFrame);
|
|
94
113
|
this._renderer.xr.setSession(session);
|
|
95
114
|
this._renderer.xr.enabled = true;
|
|
96
|
-
|
|
97
|
-
this._renderer.setPixelRatio(Math.min(2, window.devicePixelRatio));
|
|
98
|
-
this._renderer.setSize(window.innerWidth, window.innerHeight);
|
|
115
|
+
|
|
99
116
|
this._camera = new PerspectiveCamera();
|
|
100
117
|
this._scene = new Scene();
|
|
101
118
|
this._scene.fog = new Fog(0x444444, 10, 250);
|
|
102
119
|
this._scene.add(this._camera);
|
|
103
120
|
this.setupScene();
|
|
121
|
+
|
|
104
122
|
}
|
|
105
123
|
|
|
106
124
|
end() {
|
|
@@ -133,6 +151,7 @@ export class TemporaryXRContext {
|
|
|
133
151
|
}
|
|
134
152
|
|
|
135
153
|
private _lastTime = 0;
|
|
154
|
+
private _frames = 0;
|
|
136
155
|
private onFrame = (time: DOMHighResTimeStamp, _frame: XRFrame) => {
|
|
137
156
|
const dt = time - this._lastTime;
|
|
138
157
|
this.update(time, dt);
|
|
@@ -140,6 +159,39 @@ export class TemporaryXRContext {
|
|
|
140
159
|
this._scene.add(this._camera);
|
|
141
160
|
}
|
|
142
161
|
this._renderer.render(this._scene, this._camera);
|
|
162
|
+
this._lastTime = time;
|
|
163
|
+
this._frames++;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
private readonly _roomFlyObjects: Mesh[] = [];
|
|
168
|
+
private _logoObject: Mesh | null = null;
|
|
169
|
+
private get _logoDistance() {
|
|
170
|
+
return this.isAR ? 0.3 : 5;
|
|
171
|
+
}
|
|
172
|
+
private get _logoScale() {
|
|
173
|
+
return this.isAR ? 0.04 : 1;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private update(time: number, _deltaTime: number) {
|
|
177
|
+
|
|
178
|
+
const speed = time * .0004;
|
|
179
|
+
for (let i = 0; i < this._roomFlyObjects.length; i++) {
|
|
180
|
+
const obj = this._roomFlyObjects[i];
|
|
181
|
+
obj.position.y += Math.sin(speed + i * .5) * 0.005;
|
|
182
|
+
obj.rotateY(.002);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const logo = this._logoObject;
|
|
186
|
+
const xrCamera = this._renderer.xr.getCamera() as ArrayCamera;
|
|
187
|
+
if (logo) {
|
|
188
|
+
const cameraForward = new Vector3();
|
|
189
|
+
xrCamera.getWorldDirection(cameraForward);
|
|
190
|
+
const targetPosition = xrCamera.position.clone().addScaledVector(cameraForward, this._logoDistance);
|
|
191
|
+
const speed = this.isAR ? 0.005 : 0.00001; // in VR it's nicer to have the logo basically static
|
|
192
|
+
logo.position.lerp(targetPosition, this._frames <= 2 ? 1 : _deltaTime * speed);
|
|
193
|
+
logo.lookAt(this._camera.position);
|
|
194
|
+
}
|
|
143
195
|
}
|
|
144
196
|
|
|
145
197
|
/** can be used to prepare the user or fade to black */
|
|
@@ -159,10 +211,78 @@ export class TemporaryXRContext {
|
|
|
159
211
|
}
|
|
160
212
|
|
|
161
213
|
|
|
162
|
-
private _objects: Mesh[] = [];
|
|
163
214
|
private setupScene() {
|
|
164
215
|
this._scene.background = new Color(0x000000);
|
|
165
|
-
|
|
216
|
+
|
|
217
|
+
let logoSrc = needleLogoOnlySVG;
|
|
218
|
+
if (hasCommercialLicense()) {
|
|
219
|
+
const htmlComponent = document.querySelector("needle-engine");
|
|
220
|
+
if (htmlComponent) {
|
|
221
|
+
const licenseLogo = htmlComponent.getAttribute("logo-src");
|
|
222
|
+
if (licenseLogo?.length) {
|
|
223
|
+
logoSrc = licenseLogo;
|
|
224
|
+
if (isDevEnvironment()) console.debug("[XR] Using custom loading logo from license:", logoSrc);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const logo = this._logoObject = new Mesh(new PlaneGeometry(1, 1, 1, 1), new MeshBasicMaterial({ transparent: true, side: 2 }));
|
|
229
|
+
logo.scale.multiplyScalar(this._logoScale * window.devicePixelRatio);
|
|
230
|
+
logo.renderOrder = 1000;
|
|
231
|
+
logo.material.opacity = 0;
|
|
232
|
+
this._scene.add(logo);
|
|
233
|
+
const canvas = document.createElement("canvas");
|
|
234
|
+
const ctx = canvas.getContext("2d");
|
|
235
|
+
const img = new Image();
|
|
236
|
+
const drawLogo = (loadingFailed: boolean) => {
|
|
237
|
+
if (!ctx) return;
|
|
238
|
+
logo.material.opacity = 1;
|
|
239
|
+
const size = 1024;
|
|
240
|
+
canvas.width = size;
|
|
241
|
+
canvas.height = size;
|
|
242
|
+
ctx.imageSmoothingQuality = "high";
|
|
243
|
+
|
|
244
|
+
// ctx.fillStyle = "#33333399";
|
|
245
|
+
// ctx.fillRect(0, 0, canvas.width, canvas.height,);
|
|
246
|
+
|
|
247
|
+
const padding = size * .19;
|
|
248
|
+
const aspect = loadingFailed ? 1 : img.width / img.height;
|
|
249
|
+
if (!loadingFailed) {
|
|
250
|
+
const maxHeight = canvas.height - padding * 1.5;
|
|
251
|
+
const imgWidth = maxHeight * aspect;
|
|
252
|
+
const imgX = (canvas.width - imgWidth) / 2;
|
|
253
|
+
ctx.drawImage(img, imgX, 0, imgWidth, maxHeight);
|
|
254
|
+
}
|
|
255
|
+
const fontSize = size * .12;
|
|
256
|
+
const text = "Loading...";
|
|
257
|
+
ctx.shadowBlur = 0;
|
|
258
|
+
ctx.fillStyle = this.isAR ? "white" : "rgba(255,255,255,0.4)";
|
|
259
|
+
ctx.font = `${fontSize}px Arial`;
|
|
260
|
+
ctx.shadowBlur = size * .02;
|
|
261
|
+
ctx.shadowColor = "rgba(0,0,0,0.5)";
|
|
262
|
+
ctx.shadowOffsetX = 0;
|
|
263
|
+
ctx.shadowOffsetY = 0;
|
|
264
|
+
const metrics = ctx.measureText(text);
|
|
265
|
+
ctx.fillText(text, canvas.width / 2 - metrics.width / 2, canvas.height - padding / 4);
|
|
266
|
+
ctx.font = `${fontSize}px Arial`;
|
|
267
|
+
ctx.fillText(text, canvas.width / 2 - metrics.width / 2, canvas.height - padding / 4);
|
|
268
|
+
|
|
269
|
+
const texture = new TextureLoader().load(canvas.toDataURL());
|
|
270
|
+
texture.generateMipmaps = true;
|
|
271
|
+
texture.colorSpace = 'srgb';
|
|
272
|
+
texture.anisotropy = 4;
|
|
273
|
+
const canvasAspect = canvas.width / canvas.height;
|
|
274
|
+
logo.scale.x = this._logoScale * canvasAspect * window.devicePixelRatio;
|
|
275
|
+
logo.scale.y = this._logoScale * window.devicePixelRatio;
|
|
276
|
+
logo.material.map = texture;
|
|
277
|
+
logo.material.needsUpdate = true;
|
|
278
|
+
}
|
|
279
|
+
img.onload = () => drawLogo(false);
|
|
280
|
+
img.onerror = e => {
|
|
281
|
+
console.error("Failed to load temporary XR logo:", logoSrc, e);
|
|
282
|
+
img.src = needleLogoOnlySVG;
|
|
283
|
+
};
|
|
284
|
+
img.crossOrigin = "anonymous";
|
|
285
|
+
img.src = logoSrc;
|
|
166
286
|
|
|
167
287
|
const light = new DirectionalLight(0xffffff, 1);
|
|
168
288
|
light.position.set(0, 20, 0);
|
|
@@ -180,48 +300,36 @@ export class TemporaryXRContext {
|
|
|
180
300
|
light3.distance = 200;
|
|
181
301
|
this._scene.add(light3);
|
|
182
302
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
material.emissive = new Color(Math.random(), Math.random(), Math.random());
|
|
193
|
-
material.emissiveIntensity = Math.random();
|
|
194
|
-
}
|
|
195
|
-
const type = Mathf.random(0, 1) > .5 ? PrimitiveType.Sphere : PrimitiveType.Cube;
|
|
196
|
-
const obj = ObjectUtils.createPrimitive(type, {
|
|
197
|
-
material
|
|
198
|
-
});
|
|
199
|
-
obj.position.x = Mathf.random(-range, range);
|
|
200
|
-
obj.position.y = Mathf.random(-2, range);
|
|
201
|
-
obj.position.z = Mathf.random(-range, range);
|
|
202
|
-
// random rotation
|
|
203
|
-
obj.rotation.x = Mathf.random(0, Math.PI * 2);
|
|
204
|
-
obj.rotation.y = Mathf.random(0, Math.PI * 2);
|
|
205
|
-
obj.rotation.z = Mathf.random(0, Math.PI * 2);
|
|
206
|
-
obj.scale.multiplyScalar(.5 + Math.random() * 10);
|
|
207
|
-
|
|
208
|
-
const dist = obj.position.distanceTo(this._camera.position) - obj.scale.x;
|
|
209
|
-
if (dist < 1) {
|
|
210
|
-
obj.position.multiplyScalar(1 + 1 / dist);
|
|
211
|
-
}
|
|
303
|
+
// if we're in passthrough
|
|
304
|
+
if (this.isAR === false) {
|
|
305
|
+
const range = 50;
|
|
306
|
+
for (let i = 0; i < 100; i++) {
|
|
307
|
+
const material = new MeshStandardMaterial({
|
|
308
|
+
color: 0x222222,
|
|
309
|
+
metalness: 1,
|
|
310
|
+
roughness: .8,
|
|
311
|
+
});
|
|
212
312
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
313
|
+
const type = PrimitiveType.Sphere; //Mathf.random(0, 1) > .5 ? PrimitiveType.Sphere : PrimitiveType.Cube;
|
|
314
|
+
const obj = ObjectUtils.createPrimitive(type, { material });
|
|
315
|
+
obj.position.x = Mathf.random(-range, range);
|
|
316
|
+
obj.position.y = Mathf.random(-2, range);
|
|
317
|
+
obj.position.z = Mathf.random(-range, range);
|
|
318
|
+
// random rotation
|
|
319
|
+
obj.rotation.x = Mathf.random(0, Math.PI * 2);
|
|
320
|
+
obj.rotation.y = Mathf.random(0, Math.PI * 2);
|
|
321
|
+
obj.rotation.z = Mathf.random(0, Math.PI * 2);
|
|
322
|
+
obj.scale.multiplyScalar(.5 + Math.random() * 10);
|
|
217
323
|
|
|
218
|
-
|
|
324
|
+
const dist = obj.position.distanceTo(this._camera.position) - obj.scale.x;
|
|
325
|
+
if (dist < 10) {
|
|
326
|
+
obj.position.z += 5;
|
|
327
|
+
obj.position.multiplyScalar(1 + 1 / dist);
|
|
328
|
+
}
|
|
219
329
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
obj.position.y += Math.sin(speed + i * .5) * 0.005;
|
|
224
|
-
obj.rotateY(.002);
|
|
330
|
+
this._roomFlyObjects.push(obj);
|
|
331
|
+
this._scene.add(obj);
|
|
332
|
+
}
|
|
225
333
|
}
|
|
226
334
|
}
|
|
227
335
|
}
|
|
@@ -229,11 +229,12 @@ export class SpectatorCamera extends Behaviour {
|
|
|
229
229
|
onLeaveXR(_evt) {
|
|
230
230
|
this.context.removeCamera(this.cam as ICamera);
|
|
231
231
|
GameObject.setActive(this.gameObject, false);
|
|
232
|
-
if (this.orbit) this.orbit.enabled = true;
|
|
233
232
|
this._handler?.set(undefined);
|
|
234
233
|
this._handler?.disable();
|
|
235
234
|
if (this.isSpectatingSelf)
|
|
236
235
|
this.stopSpectating();
|
|
236
|
+
// Importantly re-enable orbit controls on main camera at the end here. This is a workaround for https://linear.app/needle/issue/NE-6897
|
|
237
|
+
if (this.orbit) this.orbit.enabled = true;
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
/**
|