Needle Engine

Changes between version 3.32.10-alpha and 3.32.11-alpha
Files changed (7) hide show
  1. src/engine-components/DragControls.ts +1 -0
  2. src/engine/engine_input.ts +20 -11
  3. src/engine-components/ui/EventSystem.ts +1 -0
  4. src/engine/xr/NeedleXRController.ts +42 -24
  5. src/engine/xr/NeedleXRSession.ts +5 -1
  6. src/engine-components/ui/PointerEvents.ts +3 -0
  7. src/engine-components/webxr/WebXRButtons.ts +25 -4
src/engine-components/DragControls.ts CHANGED
@@ -136,6 +136,7 @@
136
136
 
137
137
  onPointerDown(args: PointerEventData) {
138
138
  if (!this.allowEdit(this.gameObject)) return;
139
+ if (args.used) return;
139
140
  DragControls.lastHovered = args.object;
140
141
 
141
142
  if (args.button === 0) {
src/engine/engine_input.ts CHANGED
@@ -58,16 +58,6 @@
58
58
 
59
59
  export class NEPointerEvent extends PointerEvent {
60
60
 
61
- /** Unique identifier for this input: a combination of the deviceIndex + button to uniquely identify the exact input (e.g. LeftController:Button0 = 0, RightController:Button1 = 101) */
62
- override readonly pointerId!: number;
63
-
64
- // this is set via the init arguments (we override it here for intellisense to show the string options)
65
- override readonly pointerType!: PointerTypeNames;
66
-
67
- // this is set via the init arguments (we override it here for intellisense to show the string options)
68
- /** The input that raised this event like `pointerdown` */
69
- override readonly type!: InputEventNames;
70
-
71
61
  /** the device index: mouse and touch are always 0, otherwise e.g. index of the connected Gamepad or XRController */
72
62
  readonly deviceIndex: number;
73
63
 
@@ -94,8 +84,27 @@
94
84
  isDoubleClick: boolean = false;
95
85
 
96
86
 
87
+ /** Unique identifier for this input: a combination of the deviceIndex + button to uniquely identify the exact input (e.g. LeftController:Button0 = 0, RightController:Button1 = 101) */
88
+ override get pointerId(): number { return this._pointerid; }
89
+ private readonly _pointerid;
90
+
91
+ // this is set via the init arguments (we override it here for intellisense to show the string options)
92
+ override get pointerType(): PointerTypeNames { return this._pointerType; }
93
+ private readonly _pointerType: PointerTypeNames;
94
+
95
+ // this is set via the init arguments (we override it here for intellisense to show the string options)
96
+ /** The input that raised this event like `pointerdown` */
97
+ override get type(): InputEventNames { return this._type; }
98
+ private readonly _type: InputEventNames;
99
+
97
100
  constructor(type: InputEvents | InputEventNames, source: Event | null, init: NEPointerEventInit) {
98
- super(type, init)
101
+ super(type, init);
102
+ // apply the init arguments. Otherwise the arguments will be undefined in the bundled / published version of needle engine
103
+ // so we have to be careful if we override properties - we then also need to set them in the constructor
104
+ this._pointerid = init.pointerId;
105
+ this._pointerType = init.pointerType;
106
+ this._type = type;
107
+
99
108
  this.deviceIndex = init.deviceIndex;
100
109
  this.origin = init.origin;
101
110
  this.source = source;
src/engine-components/ui/EventSystem.ts CHANGED
@@ -521,6 +521,7 @@
521
521
  const comp = behaviour as any;
522
522
 
523
523
  if (comp.interactable === false) return;
524
+ if (!comp.activeAndEnabled || !comp.enabled) return;
524
525
 
525
526
  if (comp.onPointerEnter) {
526
527
  if (hoveredObjectChanged) {
src/engine/xr/NeedleXRController.ts CHANGED
@@ -126,6 +126,7 @@
126
126
 
127
127
  private readonly _gripPosition = new Vector3();
128
128
  private readonly _gripQuaternion = new Quaternion();
129
+ private readonly _linearVelocity: Vector3 = new Vector3();
129
130
  private readonly _rayPosition = new Vector3();
130
131
  private readonly _rayQuaternion = new Quaternion();
131
132
 
@@ -133,6 +134,12 @@
133
134
  get gripPosition() { return getTempVector(this._gripPosition).applyMatrix4(flipForwardMatrix) }
134
135
  /** Grip rotation in rig space */
135
136
  get gripQuaternion() { return getTempQuaternion(this._gripQuaternion).premultiply(flipForwardQuaternion) }
137
+ /** Grip linear velocity in rig space
138
+ * @link https://developer.mozilla.org/en-US/docs/Web/API/XRPose/linearVelocity
139
+ */
140
+ get gripLinearVelocity() {
141
+ return getTempVector(this._linearVelocity).applyQuaternion(flipForwardQuaternion);
142
+ }
136
143
  /** Ray position in rig space */
137
144
  get rayPosition() { return getTempVector(this._rayPosition).applyMatrix4(flipForwardMatrix) }
138
145
  /** Ray rotation in rig space */
@@ -140,38 +147,27 @@
140
147
 
141
148
  /** Controller grip position in worldspace */
142
149
  get gripWorldPosition() {
143
- const v = getTempVector(this._gripPosition);
144
- const space = this.xr.context.mainCamera?.parent;
145
- if (!space) return v;
146
- return v.applyMatrix4(space.matrixWorld);
150
+ return getTempVector(this._gripWorldPosition);
147
151
  }
152
+ private readonly _gripWorldPosition: Vector3 = new Vector3();
153
+
148
154
  /** Controller grip rotation in wordspace */
149
155
  get gripWorldQuaternion() {
150
- const q = getTempQuaternion(this._gripQuaternion);
151
- // flip forward because we want +Z to be forward
152
- q.multiply(flipForwardQuaternion);
153
- const space = this.xr.context.mainCamera?.parent;
154
- if (!space) return q;
155
- q.premultiply(getWorldQuaternion(space))
156
- return q;
156
+ return getTempQuaternion(this._gripWorldQuaternion);
157
157
  }
158
+ private readonly _gripWorldQuaternion: Quaternion = new Quaternion();
159
+
158
160
  /** Controller ray position in worldspace */
159
161
  get rayWorldPosition() {
160
- const v = getTempVector(this._rayPosition);
161
- const space = this.xr.context.mainCamera?.parent;
162
- if (!space) return v;
163
- return v.applyMatrix4(space.matrixWorld);
162
+ return getTempVector(this._rayWorldPosition);
164
163
  }
164
+ private readonly _rayWorldPosition: Vector3 = new Vector3();
165
+
165
166
  /** Controller ray rotation in wordspace */
166
167
  get rayWorldQuaternion() {
167
- const q = getTempQuaternion(this._rayQuaternion)
168
- // flip forward because we want +Z to be forward
169
- .multiply(flipForwardQuaternion);
170
- const space = this.xr.context.mainCamera?.parent;
171
- if (!space) return q;
172
- q.premultiply(getWorldQuaternion(space))
173
- return q;
168
+ return getTempQuaternion(this._rayWorldQuaternion);
174
169
  }
170
+ private readonly _rayWorldQuaternion: Quaternion = new Quaternion();
175
171
 
176
172
  /** The controller ray in worldspace */
177
173
  get ray(): Ray {
@@ -181,6 +177,7 @@
181
177
  }
182
178
  private readonly _ray;
183
179
 
180
+
184
181
  /** The controller object space.
185
182
  * You can use it to attach objects to the controller.
186
183
  * Children will be automatically detached and put into the scene when the controller disconnects
@@ -246,8 +243,6 @@
246
243
  return;
247
244
  }
248
245
 
249
- // TODO: we might actually want to apply the rotation here now already to avoid the matrix multiplications in the vector and quaternion getters since we now ALWAYS deal witht the rotated data (previously the camera was rotated before calling the update methods hence we needed other data etc but this has been changed in 99a8b96fe03676078e194f5504743576a19a9b1a and now the camera is rotated at the very end of the frame - or at least it should be - which also fixed the issue with selectstart controller events requiring other frame data etc)
250
-
251
246
  const rayPose = frame.getPose(this.inputSource.targetRaySpace, this.xr.referenceSpace);
252
247
  this._isTracking = rayPose != null;
253
248
 
@@ -263,6 +258,8 @@
263
258
  const t = gripPose.transform;
264
259
  this._gripPosition.set(t.position.x, t.position.y, t.position.z);
265
260
  this._gripQuaternion.set(t.orientation.x, t.orientation.y, t.orientation.z, t.orientation.w);
261
+ if (gripPose.linearVelocity)
262
+ this._linearVelocity.set(gripPose.linearVelocity.x, gripPose.linearVelocity.y, gripPose.linearVelocity.z);
266
263
  }
267
264
  }
268
265
 
@@ -312,6 +309,27 @@
312
309
  this._object.position.copy(this._rayPosition);
313
310
  this._object.quaternion.copy(this._rayQuaternion).multiply(flipForwardQuaternion);
314
311
  }
312
+
313
+
314
+ // UPDATE WORLD TRANSFORM DATA
315
+ const parent = this.xr.context.mainCamera?.parent;
316
+ const parentWorldQuaternion = parent ? getWorldQuaternion(parent) : undefined;
317
+
318
+ // GRIP
319
+ this._gripWorldPosition.copy(this._gripPosition);
320
+ if (parent) this._gripWorldPosition.applyMatrix4(parent.matrixWorld);
321
+ this._gripWorldQuaternion.copy(this._gripQuaternion);
322
+ // flip forward because we want +Z to be forward
323
+ this._gripWorldQuaternion.multiply(flipForwardQuaternion);
324
+ if (parentWorldQuaternion) this._gripWorldQuaternion.premultiply(parentWorldQuaternion)
325
+
326
+ // RAY
327
+ this._rayWorldPosition.copy(this._rayPosition);
328
+ if (parent) this._rayWorldPosition.applyMatrix4(parent.matrixWorld);
329
+ this._rayWorldQuaternion.copy(this._rayQuaternion)
330
+ // flip forward because we want +Z to be forward
331
+ .multiply(flipForwardQuaternion);
332
+ if (parentWorldQuaternion) this._rayWorldQuaternion.premultiply(parentWorldQuaternion)
315
333
  }
316
334
 
317
335
  /** Called when the input source disconnects */
src/engine/xr/NeedleXRSession.ts CHANGED
@@ -278,7 +278,11 @@
278
278
  }
279
279
  }
280
280
 
281
- /** start a new webXR session (make sure to stop already running sessions before calling this method) */
281
+ /** start a new webXR session (make sure to stop already running sessions before calling this method)
282
+ * @param mode The XRSessionMode to start (e.g. `immersive-vr` or `immersive-ar`), docs: https://developer.mozilla.org/en-US/docs/Web/API/XRSessionMode
283
+ * @param init The XRSessionInit to use (optional), docs: https://developer.mozilla.org/en-US/docs/Web/API/XRSessionInit
284
+ * @param context The Needle Engine context to use
285
+ */
282
286
  static async start(mode: XRSessionMode, init?: XRSessionInit, context?: Context): Promise<NeedleXRSession | null> {
283
287
 
284
288
  if (this._currentSessionRequest) {
src/engine-components/ui/PointerEvents.ts CHANGED
@@ -177,6 +177,9 @@
177
177
  */
178
178
  export function hasPointerEventComponent(obj: Object3D, event?: InputEventNames | null) {
179
179
  const res = GameObject.foreachComponent(obj, comp => {
180
+ // ignore disabled components
181
+ if (!comp.enabled) return undefined;
182
+
180
183
  const handler = comp as IPointerEventHandler;
181
184
  // if a specific event is passed in, we only check for that event
182
185
  if (event) {
src/engine-components/webxr/WebXRButtons.ts CHANGED
@@ -107,7 +107,10 @@
107
107
 
108
108
  /** @returns the quicklook button if it was created */
109
109
  get quicklookButton() { return this.shadowRoot?.querySelector("[data-needle='quicklook-button']") as HTMLButtonElement | null; }
110
- /** get or create the quicklook button */
110
+ /** get or create the quicklook button
111
+ * Behaviour of the button:
112
+ * - 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
113
+ */
111
114
  createQuicklookButton(): HTMLButtonElement {
112
115
  const existingButton = this.shadowRoot?.querySelector("[data-needle='quicklook-button']") as HTMLButtonElement | null;
113
116
  if (existingButton) return existingButton;
@@ -119,6 +122,9 @@
119
122
  if (usdzExporter) {
120
123
  usdzExporter.exportAsync();
121
124
  }
125
+ else {
126
+ console.warn("No USDZExporter component found in the scene");
127
+ }
122
128
  });
123
129
  this.shadowRoot?.appendChild(button);
124
130
  return button;
@@ -126,7 +132,13 @@
126
132
 
127
133
  /** @returns the WebXR AR button if it was created */
128
134
  get arButton() { return this.shadowRoot?.querySelector("[data-needle='webxr-ar-button']") as HTMLButtonElement | null; }
129
- /** get or create the WebXR AR button */
135
+ /** get or create the WebXR AR button
136
+ * @param init optional session init options
137
+ * Behaviour of the button:
138
+ * - if the device supports AR, the button will be visible and clickable
139
+ * - if the device does not support AR, the button will be hidden
140
+ * - if the device changes and now supports AR, the button will be visible
141
+ */
130
142
  createARButton(init?: XRSessionInit): HTMLButtonElement {
131
143
  const existingButton = this.shadowRoot?.querySelector("[data-needle='webxr-ar-button']") as HTMLButtonElement | null;
132
144
  if (existingButton) return existingButton;
@@ -147,7 +159,13 @@
147
159
 
148
160
  /** @returns the WebXR VR button if it was created */
149
161
  get vrButton() { return this.shadowRoot?.querySelector("[data-needle='webxr-vr-button']") as HTMLButtonElement | null; }
150
- /** get or create the WebXR VR button */
162
+ /** get or create the WebXR VR button
163
+ * @param init optional session init options
164
+ * Behaviour of the button:
165
+ * - if the device supports VR, the button will be visible and clickable
166
+ * - if the device does not support VR, the button will be hidden
167
+ * - if the device changes and now supports VR, the button will be visible
168
+ */
151
169
  createVRButton(init?: XRSessionInit): HTMLButtonElement {
152
170
  const hasButton = this.shadowRoot?.querySelector("[data-needle='webxr-vr-button']");
153
171
  if (hasButton) return hasButton as HTMLButtonElement;
@@ -168,7 +186,10 @@
168
186
 
169
187
  /** @returns the Send to Quest button */
170
188
  get sendToQuestButton() { return this.shadowRoot?.querySelector("[data-needle='webxr-sendtoquest-button']") as HTMLButtonElement | null; }
171
- /** get or create the Send To Quest button */
189
+ /** get or create the Send To Quest button
190
+ * Behaviour of the button:
191
+ * - if the button is clicked, the current URL will be sent to the Oculus Browser on the Quest
192
+ */
172
193
  createSendToQuestButton(): HTMLButtonElement {
173
194
  const hasButton = this.shadowRoot?.querySelector("[data-needle='webxr-sendtoquest-button']");
174
195
  if (hasButton) return hasButton as HTMLButtonElement;