@@ -23,15 +23,20 @@
|
|
23
23
|
readonly source: Event | null;
|
24
24
|
|
25
25
|
readonly mode: XRTargetRayMode;
|
26
|
-
/** A ray in worldspace for the event
|
26
|
+
/** A ray in worldspace for the event.
|
27
|
+
* If the ray is undefined you can also use `space.worldForward` and `space.worldPosition` */
|
27
28
|
readonly ray?: Ray;
|
28
|
-
/** The device space (this object is not necessarily rendered in the scene but you can access or copy the matrix)
|
29
|
+
/** The device space (this object is not necessarily rendered in the scene but you can access or copy the matrix)
|
30
|
+
* E.g. you can access the input world space source position with `space.worldPosition` or world direction with `space.worldForward`
|
31
|
+
*/
|
29
32
|
readonly space: IGameObject;
|
30
33
|
|
34
|
+
/** true if this event is a click */
|
31
35
|
isClick: boolean = false;
|
36
|
+
/** true if this event is a double click */
|
32
37
|
isDoubleClick: boolean = false;
|
33
38
|
|
34
|
-
constructor(type: InputEvents, source: Event | null, init: NEPointerEventInit) {
|
39
|
+
constructor(type: InputEvents | InputEventNames, source: Event | null, init: NEPointerEventInit) {
|
35
40
|
super(type, init)
|
36
41
|
this.source = source;
|
37
42
|
this.mode = init.mode;
|
@@ -109,7 +114,7 @@
|
|
109
114
|
KeyUp = "keyup",
|
110
115
|
KeyPressed = "keypress"
|
111
116
|
}
|
112
|
-
type InputEventNames = EnumToPrimitiveUnion<InputEvents>;
|
117
|
+
export type InputEventNames = EnumToPrimitiveUnion<InputEvents>;
|
113
118
|
|
114
119
|
|
115
120
|
declare type PointerEventListener = (evt: NEPointerEvent) => void;
|
@@ -138,7 +143,7 @@
|
|
138
143
|
if (listeners) {
|
139
144
|
for (const l of listeners) {
|
140
145
|
if (evt.immediatePropagationStopped) {
|
141
|
-
if(debug) console.log("immediatePropagationStopped", evt.type);
|
146
|
+
if (debug) console.log("immediatePropagationStopped", evt.type);
|
142
147
|
break;
|
143
148
|
}
|
144
149
|
l(evt);
|
@@ -571,7 +576,7 @@
|
|
571
576
|
private onTouchStartWindow = (evt: TouchEvent) => {
|
572
577
|
// onTouchStart registers on the renderer canvas so that we don't receive events when clicking on UI elements slotted in Needle Engine
|
573
578
|
// however in AR we need to handle these events on the window since they're not emitted otherwise (see NE-4098)
|
574
|
-
if(!this.context.isInAR) return;
|
579
|
+
if (!this.context.isInAR) return;
|
575
580
|
this.onTouchStart(evt);
|
576
581
|
};
|
577
582
|
private onTouchStart = (evt: TouchEvent) => {
|
@@ -608,7 +613,7 @@
|
|
608
613
|
|
609
614
|
if (debug) showBalloonMessage(`touch up #${id}, identifier:${touch.identifier}`);
|
610
615
|
const space = this.getAndUpdateSpatialObjectForScreenPosition(id, touch.clientX, touch.clientY);
|
611
|
-
const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { mode: "screen", pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch, buttonName: "unknown", device:space });
|
616
|
+
const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { mode: "screen", pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: PointerType.Touch, buttonName: "unknown", device: space });
|
612
617
|
this.onUp(ne);
|
613
618
|
}
|
614
619
|
}
|
@@ -660,7 +665,7 @@
|
|
660
665
|
private readonly tempLookMatrix = new Matrix4();
|
661
666
|
private getAndUpdateSpatialObjectForScreenPosition(id: number, screenX: number, screenY: number): IGameObject {
|
662
667
|
let space = this._pointerSpace[id]
|
663
|
-
if (!space) {
|
668
|
+
if (!space) {
|
664
669
|
space = new Object3D() as unknown as IGameObject;
|
665
670
|
this._pointerSpace[id] = space;
|
666
671
|
}
|
@@ -785,7 +790,7 @@
|
|
785
790
|
|
786
791
|
|
787
792
|
if (Math.abs(dx) < 5 && Math.abs(dy) < 5) {
|
788
|
-
if(debug) console.log("CLICK", index)
|
793
|
+
if (debug) console.log("CLICK", index)
|
789
794
|
this.setPointerState(index, this._pointerClick, true);
|
790
795
|
evt.isClick = true;
|
791
796
|
|
@@ -3,7 +3,7 @@
|
|
3
3
|
import type { ButtonName, IGameObject, Vec3, XRControllerButtonName } from "../engine_types.js";
|
4
4
|
import { Context } from "../engine_context.js";
|
5
5
|
import { Gizmos } from "../engine_gizmos.js";
|
6
|
-
import { InputEvents, NEPointerEventInit, PointerType, NEPointerEvent } from "../engine_input.js";
|
6
|
+
import { InputEvents, NEPointerEventInit, PointerType, NEPointerEvent, InputEventNames } from "../engine_input.js";
|
7
7
|
import { getTempVector, getTempQuaternion, getWorldQuaternion } from "../engine_three_utils.js";
|
8
8
|
import type { NeedleXRHitTestResult, NeedleXRSession } from "./NeedleXRSession.js";
|
9
9
|
import { flipForwardMatrix, flipForwardQuaternion } from "./internal.js";
|
@@ -15,7 +15,7 @@
|
|
15
15
|
declare type ControllerAxes = "xr-standard-thumbstick";
|
16
16
|
declare type StickName = "xr-standard-thumbstick";
|
17
17
|
declare type Mapping = "xr-standard";
|
18
|
-
declare type ComponentType = "button" | "thumbstick";
|
18
|
+
declare type ComponentType = "button" | "thumbstick" | "squeeze";
|
19
19
|
declare type GamepadKey = "button" | "xAxis" | "yAxis";
|
20
20
|
|
21
21
|
|
@@ -68,6 +68,11 @@
|
|
68
68
|
/** the input source index */
|
69
69
|
readonly index: number = 0;
|
70
70
|
|
71
|
+
/** When enabled the controller will create input events in the Needle Engine input system (e.g. when a button is pressed or the controller is moved)
|
72
|
+
* You can disable this if you don't want inputs to go through the input system but be aware that this will result in `onPointerDown` component callbacks to not be invoked anymore for this XRController
|
73
|
+
*/
|
74
|
+
emitEvents = true;
|
75
|
+
|
71
76
|
// EXPOSE API
|
72
77
|
/**
|
73
78
|
* Is the controller still connected?
|
@@ -342,6 +347,7 @@
|
|
342
347
|
if (componentModel?.gamepadIndices) {
|
343
348
|
switch (componentModel.type) {
|
344
349
|
case "button":
|
350
|
+
case "squeeze":
|
345
351
|
if (this.inputSource.gamepad) {
|
346
352
|
const index = componentModel.gamepadIndices!.button!;
|
347
353
|
this._buttonMap.set(key, index);
|
@@ -508,29 +514,23 @@
|
|
508
514
|
// and start with index = 1
|
509
515
|
private updateInputEvents() {
|
510
516
|
if (!this._layout) return;
|
511
|
-
|
512
|
-
let index = 1;
|
513
|
-
|
517
|
+
|
514
518
|
// https://immersive-web.github.io/webxr-gamepads-module/#xr-standard-heading
|
515
519
|
if (this.gamepad?.buttons) {
|
516
520
|
for (let k = 0; k < this.gamepad.buttons.length; k++) {
|
517
|
-
// the selection event is handled in the "selectstart" callback
|
518
|
-
if (this._selectButtonIndex === k) continue;
|
519
|
-
|
520
521
|
const button = this.gamepad.buttons[k];
|
521
|
-
const
|
522
|
-
|
523
|
-
let inputEvent: InputEvents | null = null;
|
522
|
+
const state = this.states[k] || new InputState();
|
523
|
+
let eventName: InputEventNames | null = null;
|
524
524
|
|
525
525
|
// is down
|
526
526
|
if (button.pressed && !state.pressed) {
|
527
|
-
|
527
|
+
eventName = "pointerdown";
|
528
528
|
state.isDown = true;
|
529
529
|
state.isUp = false;
|
530
530
|
}
|
531
531
|
// is up
|
532
532
|
else if (!button.pressed && state.pressed) {
|
533
|
-
|
533
|
+
eventName = "pointerup"
|
534
534
|
state.isDown = false;
|
535
535
|
state.isUp = true;
|
536
536
|
}
|
@@ -541,23 +541,32 @@
|
|
541
541
|
|
542
542
|
state.value = button.value;
|
543
543
|
state.pressed = button.pressed;
|
544
|
-
this.states[
|
544
|
+
this.states[k] = state;
|
545
545
|
|
546
|
-
|
546
|
+
// the selection event is handled in the "selectstart" callback
|
547
|
+
const emitEvent = k !== this._selectButtonIndex && k !== this._squeezeButtonIndex;
|
548
|
+
|
549
|
+
if (eventName != null && emitEvent) {
|
547
550
|
const name = this._layout?.gamepad[k];
|
548
|
-
this.emitPointerEvent(
|
551
|
+
this.emitPointerEvent(eventName, k, name ?? "none", false);
|
549
552
|
}
|
550
553
|
}
|
551
554
|
}
|
552
555
|
}
|
553
556
|
private onUpdateMove() {
|
554
|
-
this.emitPointerEvent(
|
557
|
+
this.emitPointerEvent("pointermove", 0, "none", false);
|
555
558
|
}
|
556
559
|
|
557
560
|
|
558
561
|
/** cached spatial pointer init object. We re-use it to not have */
|
559
562
|
private readonly pointerInit: NEPointerEventInit;
|
560
|
-
private emitPointerEvent(type:
|
563
|
+
private emitPointerEvent(type: InputEventNames, button: number, buttonName: ButtonName | "none", primary: boolean, source: Event | null = null) {
|
564
|
+
|
565
|
+
if (!this.emitEvents) {
|
566
|
+
if(debug && type !== InputEvents.PointerMove) console.warn("Pointer events are disabled for this controller", this.index, type, button);
|
567
|
+
return;
|
568
|
+
}
|
569
|
+
|
561
570
|
// Currently we do only want to emit pointer events for NON screen based events
|
562
571
|
// that means if the input device is spatial (AR touch on a screen should be handled via touchdown etc still)
|
563
572
|
// Not sure if *this* is enough to determine if the event is spatial or not
|
@@ -323,7 +323,10 @@
|
|
323
323
|
|
324
324
|
const scripts = mode == "immersive-ar" ? context.scripts_immersive_ar : context.scripts_immersive_vr
|
325
325
|
|
326
|
-
|
326
|
+
if (debug)
|
327
|
+
console.log("%c" + `Requesting ${mode} session`, "font-weight:bold;", init, scripts);
|
328
|
+
else
|
329
|
+
console.log("%c" + `Requesting ${mode} session`, "font-weight:bold;");
|
327
330
|
for (const script of scripts) {
|
328
331
|
if (script.onBeforeXR) script.onBeforeXR(mode, init);
|
329
332
|
}
|
@@ -347,6 +350,7 @@
|
|
347
350
|
listener({ mode, init, newSession: newSession || null });
|
348
351
|
}
|
349
352
|
if (!newSession) {
|
353
|
+
console.warn("XR Session request was rejected");
|
350
354
|
return null;
|
351
355
|
}
|
352
356
|
return this.setSession(mode, newSession, init, context);
|
@@ -23,7 +23,8 @@
|
|
23
23
|
:host {
|
24
24
|
position: absolute;
|
25
25
|
display: flex;
|
26
|
-
z-index
|
26
|
+
/** increase z-index (nipplejs has 999 as default) */
|
27
|
+
z-index: 5000;
|
27
28
|
bottom: 100px;
|
28
29
|
left: 50%;
|
29
30
|
transform: translateX(-50%);
|
@@ -26,10 +26,22 @@
|
|
26
26
|
@serializable()
|
27
27
|
rotationStep = 60;
|
28
28
|
|
29
|
-
/**
|
29
|
+
/** When enabled you can teleport using the right XR controller's thumbstick by pressing forward */
|
30
30
|
@serializable()
|
31
|
+
useTeleport: boolean = true;
|
32
|
+
|
33
|
+
/** Enable to only allow teleporting on objects with a teleport target component */
|
34
|
+
@serializable()
|
31
35
|
useTeleportTarget = false;
|
32
36
|
|
37
|
+
/** enable to visualize controller rays in the 3D scene */
|
38
|
+
@serializable()
|
39
|
+
showRays: boolean = true;
|
40
|
+
|
41
|
+
/** enable to visualize pointer targets in the 3D scene */
|
42
|
+
@serializable()
|
43
|
+
showHits: boolean = true;
|
44
|
+
|
33
45
|
readonly isXRMovementHandler: true = true;
|
34
46
|
|
35
47
|
readonly xrSessionMode = "immersive-vr";
|
@@ -43,8 +55,10 @@
|
|
43
55
|
|
44
56
|
// in AR pass through mode we dont want to move the rig
|
45
57
|
if (args.xr.isPassThrough) {
|
46
|
-
this.
|
47
|
-
|
58
|
+
if (this.showRays)
|
59
|
+
this.renderRays(args.xr);
|
60
|
+
if (this.showHits)
|
61
|
+
this.renderHits(args.xr);
|
48
62
|
return;
|
49
63
|
}
|
50
64
|
|
@@ -55,11 +69,14 @@
|
|
55
69
|
this.onHandleMovement(movementController, rig.gameObject);
|
56
70
|
if (teleportController) {
|
57
71
|
this.onHandleRotation(teleportController, rig.gameObject);
|
58
|
-
this.
|
72
|
+
if (this.useTeleport)
|
73
|
+
this.onHandleTeleport(teleportController, rig.gameObject);
|
59
74
|
}
|
60
75
|
|
61
|
-
this.
|
62
|
-
|
76
|
+
if (this.showRays)
|
77
|
+
this.renderRays(args.xr);
|
78
|
+
if (this.showHits)
|
79
|
+
this.renderHits(args.xr);
|
63
80
|
}
|
64
81
|
onLeaveXR(_: NeedleXREventArgs): void {
|
65
82
|
for (const line of this._lines) {
|
@@ -149,7 +166,7 @@
|
|
149
166
|
const ctrl = session.controllers[i];
|
150
167
|
let line = this._lines[i];
|
151
168
|
if (!line) {
|
152
|
-
line =
|
169
|
+
line = this.createRayLineObject();
|
153
170
|
line.scale.z = .5;
|
154
171
|
this._lines[i] = line;
|
155
172
|
}
|
@@ -180,7 +197,7 @@
|
|
180
197
|
|
181
198
|
let disc = this._hitDiscs[i];
|
182
199
|
if (!disc) {
|
183
|
-
disc =
|
200
|
+
disc = this.createHitPointObject();
|
184
201
|
this._hitDiscs[i] = disc;
|
185
202
|
}
|
186
203
|
disc.visible = true;
|
@@ -211,72 +228,75 @@
|
|
211
228
|
}
|
212
229
|
}
|
213
230
|
}
|
214
|
-
}
|
215
231
|
|
232
|
+
/** create an object to visualize hit points in the scene */
|
233
|
+
protected createHitPointObject(): Object3D {
|
234
|
+
var container = new Object3D();
|
235
|
+
const disc = new Mesh(
|
236
|
+
new RingGeometry(.3, 0.5, 32).rotateX(- Math.PI / 2),
|
237
|
+
new MeshBasicMaterial({
|
238
|
+
color: 0xeeeeee,
|
239
|
+
opacity: .7,
|
240
|
+
transparent: true,
|
241
|
+
side: DoubleSide,
|
242
|
+
})
|
243
|
+
);
|
244
|
+
disc.layers.disableAll();
|
245
|
+
disc.layers.enable(2);
|
246
|
+
container.add(disc);
|
216
247
|
|
217
|
-
const
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
248
|
+
const disc2 = new Mesh(
|
249
|
+
new RingGeometry(.43, 0.5, 32).rotateX(- Math.PI / 2),
|
250
|
+
new MeshBasicMaterial({
|
251
|
+
color: 0x000000,
|
252
|
+
opacity: .2,
|
253
|
+
transparent: true,
|
254
|
+
side: DoubleSide,
|
255
|
+
})
|
256
|
+
);
|
257
|
+
disc2.layers.disableAll();
|
258
|
+
disc2.layers.enable(2);
|
259
|
+
disc2.position.z -= .01;
|
260
|
+
container.add(disc2);
|
261
|
+
return container;
|
262
|
+
}
|
232
263
|
|
233
|
-
|
234
|
-
|
235
|
-
new
|
236
|
-
|
237
|
-
|
238
|
-
transparent: true,
|
239
|
-
side: DoubleSide,
|
240
|
-
})
|
241
|
-
);
|
242
|
-
disc2.layers.disableAll();
|
243
|
-
disc2.layers.enable(2);
|
244
|
-
disc2.position.z -= .01;
|
245
|
-
container.add(disc2);
|
264
|
+
/** create an object to visualize controller rays */
|
265
|
+
protected createRayLineObject() {
|
266
|
+
const line = new Line2();
|
267
|
+
line.layers.disableAll();
|
268
|
+
line.layers.enable(2);
|
246
269
|
|
247
|
-
|
248
|
-
|
270
|
+
const geometry = new LineGeometry();
|
271
|
+
line.geometry = geometry;
|
249
272
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
line.layers.enable(2);
|
273
|
+
const positions = new Float32Array(9);
|
274
|
+
positions.set([0, 0, .02, 0, 0, .4, 0, 0, 1]);
|
275
|
+
geometry.setPositions(positions)
|
254
276
|
|
255
|
-
|
256
|
-
|
277
|
+
const colors = new Float32Array(9);
|
278
|
+
colors.set([1, 1, 1, .1, .1, .1, 0, 0, 0]);
|
279
|
+
geometry.setColors(colors);
|
257
280
|
|
258
|
-
|
259
|
-
|
260
|
-
|
281
|
+
const mat = new LineMaterial({
|
282
|
+
color: 0xffffff,
|
283
|
+
vertexColors: true,
|
284
|
+
worldUnits: true,
|
285
|
+
linewidth: .004,
|
261
286
|
|
262
|
-
|
263
|
-
|
264
|
-
|
287
|
+
transparent: true,
|
288
|
+
// TODO: this doesnt work with passthrough
|
289
|
+
blending: AdditiveBlending,
|
290
|
+
dashed: false,
|
291
|
+
alphaToCoverage: true,
|
265
292
|
|
266
|
-
|
267
|
-
|
268
|
-
vertexColors: true,
|
269
|
-
worldUnits: true,
|
270
|
-
linewidth: .004,
|
293
|
+
});
|
294
|
+
line.material = mat;
|
271
295
|
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
dashed: false,
|
276
|
-
alphaToCoverage: true,
|
296
|
+
return line;
|
297
|
+
}
|
298
|
+
}
|
277
299
|
|
278
|
-
});
|
279
|
-
line.material = mat;
|
280
300
|
|
281
|
-
|
282
|
-
|
301
|
+
const up = new Vector3(0, 1, 0);
|
302
|
+
|