@needle-tools/engine 4.12.2 → 4.12.3
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 +7 -0
- package/dist/{needle-engine.bundle-DrIUla7B.min.js → needle-engine.bundle-B3ssYJS0.min.js} +111 -111
- package/dist/{needle-engine.bundle-zpJK5gi0.umd.cjs → needle-engine.bundle-CLPD2ttK.umd.cjs} +114 -114
- package/dist/{needle-engine.bundle-soFsvDTp.js → needle-engine.bundle-u-rSDw6R.js} +3495 -3435
- package/dist/needle-engine.d.ts +27 -19
- 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 +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/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-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 +1 -1
- package/src/asap/needle-asap.ts +1 -1
- package/src/engine/debug/debug_overlay.ts +1 -1
- 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/needle menu/needle-menu-spatial.ts +15 -13
- package/src/engine/webcomponents/needle-engine.loading.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
|
@@ -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
|
/**
|
|
@@ -3,7 +3,7 @@ import { FrameEvent } from "../../engine/engine_setup.js";
|
|
|
3
3
|
import { DeviceUtilities, getParam } from "../../engine/engine_utils.js";
|
|
4
4
|
import { Behaviour, GameObject } from "../Component.js";
|
|
5
5
|
import { EventList } from "../EventList.js";
|
|
6
|
-
import { type IPointerEventHandler,PointerEventData } from "./PointerEvents.js";
|
|
6
|
+
import { type IPointerEventHandler, PointerEventData } from "./PointerEvents.js";
|
|
7
7
|
import { Text } from "./Text.js";
|
|
8
8
|
import { tryGetUIComponent } from "./Utils.js";
|
|
9
9
|
|
|
@@ -111,8 +111,8 @@ export class InputField extends Behaviour implements IPointerEventHandler {
|
|
|
111
111
|
this.setTextFromInputField();
|
|
112
112
|
}
|
|
113
113
|
else {
|
|
114
|
-
if(this.textComponent) this.textComponent.text = "";
|
|
115
|
-
if(this.placeholder) GameObject.setActive(this.placeholder.gameObject, true);
|
|
114
|
+
if (this.textComponent) this.textComponent.text = "";
|
|
115
|
+
if (this.placeholder) GameObject.setActive(this.placeholder.gameObject, true);
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
@@ -128,7 +128,7 @@ export class InputField extends Behaviour implements IPointerEventHandler {
|
|
|
128
128
|
|
|
129
129
|
onPointerEnter(_args: PointerEventData) {
|
|
130
130
|
const canSetCursor = _args.event.pointerType === "mouse" && _args.button === 0;
|
|
131
|
-
if(canSetCursor) this.context.input.setCursor("text");
|
|
131
|
+
if (canSetCursor) this.context.input.setCursor("text");
|
|
132
132
|
}
|
|
133
133
|
onPointerExit(_args: PointerEventData) {
|
|
134
134
|
this.context.input.unsetCursor("text")
|
|
@@ -256,10 +256,15 @@ export class InputField extends Behaviour implements IPointerEventHandler {
|
|
|
256
256
|
|
|
257
257
|
private selectInputField() {
|
|
258
258
|
if (InputField.htmlField) {
|
|
259
|
-
if (debug) console.log("Focus Inputfield", InputField.htmlFieldFocused)
|
|
259
|
+
if (debug) console.log("Focus Inputfield", InputField.htmlFieldFocused, InputField.htmlField);
|
|
260
|
+
|
|
260
261
|
InputField.htmlField.setSelectionRange(InputField.htmlField.value.length, InputField.htmlField.value.length);
|
|
261
|
-
|
|
262
|
+
|
|
263
|
+
if (DeviceUtilities.isiOS()) {
|
|
264
|
+
// in WebXR (iOS AR Safari) display can not be none to focus input it seems
|
|
265
|
+
InputField.htmlField.style.display = "block";
|
|
262
266
|
InputField.htmlField.focus({ preventScroll: true });
|
|
267
|
+
}
|
|
263
268
|
else {
|
|
264
269
|
// on Andoid if we don't focus in a timeout the keyboard will close the second time we click the InputField
|
|
265
270
|
setTimeout(() => InputField.htmlField?.focus(), 1);
|