@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/{needle-engine.bundle-DrIUla7B.min.js → needle-engine.bundle-B3ssYJS0.min.js} +111 -111
  3. package/dist/{needle-engine.bundle-zpJK5gi0.umd.cjs → needle-engine.bundle-CLPD2ttK.umd.cjs} +114 -114
  4. package/dist/{needle-engine.bundle-soFsvDTp.js → needle-engine.bundle-u-rSDw6R.js} +3495 -3435
  5. package/dist/needle-engine.d.ts +27 -19
  6. package/dist/needle-engine.js +2 -2
  7. package/dist/needle-engine.min.js +1 -1
  8. package/dist/needle-engine.umd.cjs +1 -1
  9. package/lib/asap/needle-asap.js +1 -1
  10. package/lib/asap/needle-asap.js.map +1 -1
  11. package/lib/engine/debug/debug_overlay.js +1 -1
  12. package/lib/engine/engine_components.d.ts +2 -1
  13. package/lib/engine/engine_components.js +4 -3
  14. package/lib/engine/engine_components.js.map +1 -1
  15. package/lib/engine/engine_input.d.ts +4 -1
  16. package/lib/engine/engine_input.js +24 -20
  17. package/lib/engine/engine_input.js.map +1 -1
  18. package/lib/engine/js-extensions/Object3D.d.ts +6 -2
  19. package/lib/engine/js-extensions/Object3D.js +6 -4
  20. package/lib/engine/js-extensions/Object3D.js.map +1 -1
  21. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +13 -11
  22. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
  23. package/lib/engine/webcomponents/needle-engine.loading.js +1 -1
  24. package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
  25. package/lib/engine/xr/NeedleXRSession.js +20 -0
  26. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  27. package/lib/engine/xr/TempXRContext.d.ts +7 -2
  28. package/lib/engine/xr/TempXRContext.js +143 -44
  29. package/lib/engine/xr/TempXRContext.js.map +1 -1
  30. package/lib/engine-components/SpectatorCamera.js +3 -2
  31. package/lib/engine-components/SpectatorCamera.js.map +1 -1
  32. package/lib/engine-components/ui/InputField.js +5 -2
  33. package/lib/engine-components/ui/InputField.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/asap/needle-asap.ts +1 -1
  36. package/src/engine/debug/debug_overlay.ts +1 -1
  37. package/src/engine/engine_components.ts +5 -4
  38. package/src/engine/engine_input.ts +26 -19
  39. package/src/engine/js-extensions/Object3D.ts +14 -7
  40. package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +15 -13
  41. package/src/engine/webcomponents/needle-engine.loading.ts +1 -1
  42. package/src/engine/xr/NeedleXRSession.ts +22 -0
  43. package/src/engine/xr/TempXRContext.ts +155 -47
  44. package/src/engine-components/SpectatorCamera.ts +2 -1
  45. 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
- // Set pixel ratio and size
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
- this._scene.add(new GridHelper(5, 10, 0x111111, 0x111111));
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
- const range = 50;
184
- for (let i = 0; i < 100; i++) {
185
- const material = new MeshStandardMaterial({
186
- color: 0x222222,
187
- metalness: 1,
188
- roughness: .8,
189
- });
190
- // if we're in passthrough
191
- if (this.isAR) {
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
- this._objects.push(obj);
214
- this._scene.add(obj);
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
- private update(time: number, _deltaTime: number) {
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
- const speed = time * .0004;
221
- for (let i = 0; i < this._objects.length; i++) {
222
- const obj = this._objects[i];
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
- if (DeviceUtilities.isiOS())
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);