Needle Engine

Changes between version 3.20.0 and 3.20.1
Files changed (7) hide show
  1. src/engine-components/Component.ts +5 -5
  2. src/engine/debug/debug_console.ts +1 -1
  3. src/engine/engine_components.ts +7 -2
  4. src/engine/engine_license.ts +17 -7
  5. src/engine/engine_scenetools.ts +32 -8
  6. src/engine-components/js-extensions/Object3D.ts +1 -1
  7. src/engine-components/webxr/WebXRController.ts +1141 -1141
src/engine-components/Component.ts CHANGED
@@ -186,7 +186,7 @@
186
186
  * @param go component to move the component to
187
187
  * @param instance component to move to the GO
188
188
  */
189
- public static addComponent<T extends IComponent>(go: IGameObject | Object3D, instance: T) {
189
+ public static addComponent<T extends IComponent>(go: IGameObject | Object3D, instance: T | ConstructorConcrete<T>) {
190
190
  return this.moveComponent(go, instance);
191
191
  }
192
192
 
@@ -195,7 +195,7 @@
195
195
  * @param go component to move the component to
196
196
  * @param instance component to move to the GO
197
197
  */
198
- public static moveComponent<T extends IComponent>(go: IGameObject | Object3D, instance: T) {
198
+ public static moveComponent<T extends IComponent>(go: IGameObject | Object3D, instance: T | ConstructorConcrete<T>) {
199
199
  return moveComponentInstance(go, instance);
200
200
  }
201
201
 
@@ -275,11 +275,11 @@
275
275
  // these are implemented via threejs object extensions
276
276
  abstract activeSelf: boolean;
277
277
  /** creates a new component on this gameObject */
278
- abstract addNewComponent<T>(type: Constructor<T>): T | null;
278
+ abstract addNewComponent<T>(type: ConstructorConcrete<T>): T | null;
279
279
  /** adds an existing component to this gameObject */
280
- abstract addComponent(comp: IComponent): void;
280
+ abstract addComponent<T extends IComponent>(comp: T | ConstructorConcrete<T>): void;
281
281
  abstract removeComponent(comp: Component): Component;
282
- abstract getOrAddComponent<T>(typeName: Constructor<T> | null): T;
282
+ abstract getOrAddComponent<T>(typeName: ConstructorConcrete<T> | null): T;
283
283
  abstract getComponent<T>(type: Constructor<T>): T | null;
284
284
  abstract getComponents<T>(type: Constructor<T>, arr?: T[]): Array<T>;
285
285
  abstract getComponentInChildren<T>(type: Constructor<T>): T | null;
src/engine/debug/debug_console.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  const defaultButtonIcon = "📜";
12
12
 
13
13
  const showConsole = getParam("console");
14
- const suppressConsole = getParam("noerrors");
14
+ const suppressConsole = getParam("noerrors") || getParam("noconsole");
15
15
  if (showConsole) {
16
16
  showDebugConsole();
17
17
  }
src/engine/engine_components.ts CHANGED
@@ -75,7 +75,12 @@
75
75
  return componentInstance;
76
76
  }
77
77
 
78
- export function moveComponentInstance(obj: Object3D, componentInstance: IComponent) {
78
+ export function moveComponentInstance(obj: Object3D, componentInstance: IComponent | ConstructorConcrete<IComponent>) {
79
+
80
+ if (typeof componentInstance === "function") {
81
+ return addNewComponent(obj, new componentInstance());
82
+ }
83
+
79
84
  if (componentInstance.gameObject === obj) return componentInstance;
80
85
  // TODO: update raycast array
81
86
  if (componentInstance.gameObject && componentInstance.gameObject.userData.components) {
@@ -156,7 +161,7 @@
156
161
  }
157
162
  while (parent);
158
163
  }
159
- if(!arr) return null;
164
+ if (!arr) return null;
160
165
  return arr;
161
166
  }
162
167
 
src/engine/engine_license.ts CHANGED
@@ -150,6 +150,12 @@
150
150
  const licenseElement = createLicenseElement();
151
151
  const style = createLicenseStyle();
152
152
 
153
+ const imgElement = document.createElement("img");
154
+ imgElement.src = logoSVG;
155
+ imgElement.classList.add("logo");
156
+ imgElement.style.cssText = `width: 40px; height: 40px; margin-right: 2px; vertical-align: middle; margin-bottom: 2px;`;
157
+ licenseElement.appendChild(imgElement);
158
+
153
159
  const setAndUpdateStyle = () => {
154
160
  if (!licenseElement) return;
155
161
  const parent = ctx.domElement.shadowRoot || ctx.domElement;
@@ -157,6 +163,13 @@
157
163
  parent.appendChild(licenseElement);
158
164
  if (style) parent.appendChild(style);
159
165
  }
166
+ if (imgElement.parentElement !== licenseElement) {
167
+ licenseElement.appendChild(imgElement);
168
+ }
169
+ if (imgElement.src !== logoSVG) {
170
+ imgElement.setAttribute("src", logoSVG);
171
+ imgElement.style.cssText = `width: 40px; height: 40px; margin-right: 2px; vertical-align: middle; margin-bottom: 2px;`;
172
+ }
160
173
  };
161
174
 
162
175
  // call once and then ensure
@@ -165,12 +178,6 @@
165
178
  setAndUpdateStyle();
166
179
  }, 1000);
167
180
 
168
- const svg = `<img class="logo" src="${logoSVG}" style="width: 40px; height: 40px; margin-right: 2px; vertical-align: middle; margin-bottom: 2px;"/>`;
169
- const logoElement = document.createElement("div");
170
- logoElement.innerHTML = svg;
171
- logoElement.classList.add("logo");
172
- licenseElement.appendChild(logoElement);
173
-
174
181
  const textElement = document.createElement("div");
175
182
  textElement.classList.add("text");
176
183
  // if (!isMobileDevice())
@@ -344,7 +351,7 @@
344
351
 
345
352
  ${selector}:hover .logo {
346
353
  transition: all 0.2s ease-in-out !important;
347
- transform: scale(1.02) !important;
354
+ transform: scale(1.1) !important;
348
355
  cursor: pointer !important;
349
356
  }
350
357
 
@@ -364,6 +371,9 @@
364
371
  pointer-events: all;
365
372
  transform: scale(1)
366
373
  }
374
+ ${selector} .logo:hover {
375
+ transition: none !important;
376
+ }
367
377
  }
368
378
  `
369
379
  return licenseStyle;
src/engine/engine_scenetools.ts CHANGED
@@ -34,7 +34,8 @@
34
34
  registerLoader(NeedleGltfLoader);
35
35
 
36
36
 
37
- const printGltf = utils.getParam("printGltf");
37
+ const printGltf = utils.getParam("printGltf") || utils.getParam("printgltf");
38
+ const downloadGltf = utils.getParam("downloadgltf");
38
39
 
39
40
  // const loader = new GLTFLoader();
40
41
  // registerExtensions(loader);
@@ -111,12 +112,15 @@
111
112
  try {
112
113
  loaders.addDracoAndKTX2Loaders(loader, context);
113
114
  invokeEvents(GltfLoadEventType.BeforeLoad, new GltfLoadEvent(context, path, loader));
114
- loader.parse(data, path, async data => {
115
- invokeEvents(GltfLoadEventType.AfterLoaded, new GltfLoadEvent(context, path, loader, data));
116
- await handleLoadedGltf(context, path, data, seed, componentsExtension);
117
- invokeEvents(GltfLoadEventType.FinishedSetup, new GltfLoadEvent(context, path, loader, data));
118
- registerPrewarmObject(data.scene, context);
119
- resolve(data);
115
+ loader.parse(data, path, async res => {
116
+ invokeEvents(GltfLoadEventType.AfterLoaded, new GltfLoadEvent(context, path, loader, res));
117
+ await handleLoadedGltf(context, path, res, seed, componentsExtension);
118
+ invokeEvents(GltfLoadEventType.FinishedSetup, new GltfLoadEvent(context, path, loader, res));
119
+ registerPrewarmObject(res.scene, context);
120
+ resolve(res);
121
+ if (downloadGltf) {
122
+ _downloadGltf(data)
123
+ }
120
124
 
121
125
  }, err => {
122
126
  console.error("failed loading " + path, err);
@@ -149,7 +153,9 @@
149
153
  invokeEvents(GltfLoadEventType.FinishedSetup, new GltfLoadEvent(context, url, loader, data));
150
154
  registerPrewarmObject(data.scene, context);
151
155
  resolve(data);
152
-
156
+ if (downloadGltf) {
157
+ _downloadGltf(url)
158
+ }
153
159
  }, evt => {
154
160
  prog?.call(loader, evt);
155
161
  }, err => {
@@ -164,6 +170,7 @@
164
170
  });
165
171
  }
166
172
 
173
+
167
174
  function checkIfUserAttemptedToLoadALocalFile(url: string) {
168
175
  const fullurl = new URL(url, window.location.href).href;
169
176
  if (fullurl.startsWith("file://")) {
@@ -173,6 +180,23 @@
173
180
  }
174
181
  }
175
182
 
183
+ function _downloadGltf(data: string | ArrayBuffer) {
184
+ if (typeof data === "string") {
185
+ const a = document.createElement("a") as HTMLAnchorElement;
186
+ a.href = data;
187
+ a.download = data.split("/").pop()!;
188
+ a.click();
189
+ }
190
+ else {
191
+ const blob = new Blob([data], { type: "application/octet-stream" });
192
+ const url = window.URL.createObjectURL(blob);
193
+ const a = document.createElement("a") as HTMLAnchorElement;
194
+ a.href = url;
195
+ a.download = "download.glb";
196
+ a.click();
197
+ }
198
+ }
199
+
176
200
  // TODO: save references in guid map
177
201
  // const guidMap = {};
178
202
 
src/engine-components/js-extensions/Object3D.ts CHANGED
@@ -24,7 +24,7 @@
24
24
  destroy(this);
25
25
  }
26
26
 
27
- Object3D.prototype["addComponent"] = function <T extends IComponent>(instance: T) {
27
+ Object3D.prototype["addComponent"] = function <T extends IComponent>(instance: T | ConstructorConcrete<T>) {
28
28
  return moveComponentInstance(this, instance);
29
29
  }
30
30
 
src/engine-components/webxr/WebXRController.ts CHANGED
@@ -1,1141 +1,1141 @@
1
- import { BoxHelper, BufferGeometry, Color, Euler, Group, Intersection, Layers, Line, LineBasicMaterial, Material, Mesh, MeshBasicMaterial, Object3D, PerspectiveCamera, Quaternion, Ray, SphereGeometry, Vector2, Vector3 } from "three";
2
- import { OculusHandModel } from 'three/examples/jsm/webxr/OculusHandModel.js';
3
- import { OculusHandPointerModel } from 'three/examples/jsm/webxr/OculusHandPointerModel.js';
4
- import { XRControllerModel, XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js';
5
- import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
6
-
7
- import { InstancingUtil } from "../../engine/engine_instancing.js";
8
- import { Mathf } from "../../engine/engine_math.js";
9
- import { RaycastOptions } from "../../engine/engine_physics.js";
10
- import { getWorldPosition, getWorldQuaternion, setWorldPosition, setWorldQuaternion } from "../../engine/engine_three_utils.js";
11
- import { getParam, resolveUrl } from "../../engine/engine_utils.js";
12
- import { addDracoAndKTX2Loaders } from "../../engine/engine_loaders.js";
13
-
14
- import { Avatar_POI } from "../avatar/Avatar_Brain_LookAt.js";
15
- import { Behaviour, GameObject } from "../Component.js";
16
- import { Interactable, UsageMarker } from "../Interactable.js";
17
- import { Rigidbody } from "../RigidBody.js";
18
- import { SyncedTransform } from "../SyncedTransform.js";
19
- import { UIRaycastUtils } from "../ui/RaycastUtils.js";
20
- import { WebXR } from "./WebXR.js";
21
- import { XRRig } from "./WebXRRig.js";
22
-
23
- const debug = getParam("debugwebxrcontroller");
24
-
25
- export enum ControllerType {
26
- PhysicalDevice = 0,
27
- Touch = 1,
28
- }
29
-
30
- export enum ControllerEvents {
31
- SelectStart = "select-start",
32
- SelectEnd = "select-end",
33
- Update = "update",
34
- }
35
-
36
- export class TeleportTarget extends Behaviour {
37
-
38
- }
39
-
40
- export class WebXRController extends Behaviour {
41
-
42
- public static Factory: XRControllerModelFactory = new XRControllerModelFactory();
43
-
44
- private static raycastColor: Color = new Color(.9, .3, .3);
45
- private static raycastNoHitColor: Color = new Color(.6, .6, .6);
46
- private static geometry = new BufferGeometry().setFromPoints([new Vector3(0, 0, 0), new Vector3(0, 0, -1)]);
47
- private static handModels: { [index: number]: OculusHandPointerModel } = {};
48
-
49
- private static CreateRaycastLine(): Line {
50
- const line = new Line(this.geometry);
51
- const mat = line.material as LineBasicMaterial;
52
- mat.color = this.raycastColor;
53
- // mat.linewidth = 10;
54
- line.layers.set(2);
55
- line.name = 'line';
56
- line.scale.z = 1;
57
- return line;
58
- }
59
-
60
- private static CreateRaycastHitPoint(): Mesh {
61
- const geometry = new SphereGeometry(.5, 22, 22);
62
- const material = new MeshBasicMaterial({ color: this.raycastColor });
63
- const sphere = new Mesh(geometry, material);
64
- sphere.visible = false;
65
- sphere.layers.set(2);
66
- return sphere;
67
- }
68
-
69
- public static Create(owner: WebXR, index: number, addTo: GameObject, type: ControllerType): WebXRController {
70
- const ctrl = addTo ? GameObject.addNewComponent(addTo, WebXRController, false) : new WebXRController();
71
-
72
- ctrl.webXR = owner;
73
- ctrl.index = index;
74
- ctrl.type = type;
75
-
76
- const context = owner.context;
77
- // from https://github.com/mrdoob/js/blob/master/examples/webxr_vr_dragging.html
78
- // controllers
79
- ctrl.controller = context.renderer.xr.getController(index);
80
- ctrl.controllerGrip = context.renderer.xr.getControllerGrip(index);
81
- ctrl.controllerModel = this.Factory.createControllerModel(ctrl.controller);
82
- ctrl.controllerGrip.add(ctrl.controllerModel);
83
-
84
- ctrl.hand = context.renderer.xr.getHand(index);
85
-
86
- const loader = new GLTFLoader();
87
- addDracoAndKTX2Loaders(loader, context);
88
- if (ctrl.webXR.handModelPath && ctrl.webXR.handModelPath !== "")
89
- loader.setPath(resolveUrl(owner.sourceId, ctrl.webXR.handModelPath));
90
- else
91
- // from XRHandMeshModel.js
92
- loader.setPath('https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles/generic-hand/');
93
- //@ts-ignore
94
- const hand = new OculusHandModel(ctrl.hand, loader);
95
-
96
- ctrl.hand.add(hand);
97
- ctrl.hand.traverse(x => x.layers.set(2));
98
-
99
- ctrl.handPointerModel = new OculusHandPointerModel(ctrl.hand, ctrl.controller);
100
-
101
-
102
- // TODO remove all these once https://github.com/mrdoob/js/pull/23279 lands
103
- ctrl.controller.addEventListener('connected', (_) => {
104
- ctrl.setControllerLayers(ctrl.controllerModel, 2);
105
- ctrl.setControllerLayers(ctrl.controllerGrip, 2);
106
- ctrl.setControllerLayers(ctrl.hand, 2);
107
- setTimeout(() => {
108
- ctrl.setControllerLayers(ctrl.controllerModel, 2);
109
- ctrl.setControllerLayers(ctrl.controllerGrip, 2);
110
- ctrl.setControllerLayers(ctrl.hand, 2);
111
- }, 1000);
112
- });
113
-
114
- // TODO: unsubscribe! this should be moved into onenable and ondisable!
115
- // TODO remove all these once https://github.com/mrdoob/js/pull/23279 lands
116
- ctrl.hand.addEventListener('connected', (event) => {
117
- const xrInputSource = event.data;
118
- if (xrInputSource.hand) {
119
- if (owner.Rig) owner.Rig.add(ctrl.hand);
120
- ctrl.type = ControllerType.PhysicalDevice;
121
- ctrl.handPointerModel.traverse(x => x.layers.set(2)); // ignore raycast
122
- ctrl.handPointerModel.pointerObject?.traverse(x => x.layers.set(2));
123
-
124
- // when exiting and re-entering xr the joints are not parented to the hand anymore
125
- // this is a workaround to fix that temporarely
126
- // see https://github.com/needle-tools/needle-tiny-playground/issues/123
127
- const jnts = ctrl.hand["joints"];
128
- if (jnts) {
129
- for (const key of Object.keys(jnts)) {
130
- const joint = jnts[key];
131
- if (joint.parent) continue;
132
- ctrl.hand.add(joint);
133
- }
134
- }
135
- }
136
- });
137
-
138
- return ctrl;
139
- }
140
-
141
- // TODO: replace with component events
142
- public static addEventListener(evt: ControllerEvents, callback: (controller: WebXRController, args: any) => void) {
143
- const list = this.eventSubs[evt] ?? [];
144
- list.push(callback);
145
- this.eventSubs[evt] = list;
146
- }
147
-
148
- // TODO: replace with component events
149
- public static removeEventListener(evt: ControllerEvents, callback: (controller: WebXRController, args: any) => void) {
150
- if (!callback) return;
151
- const list = this.eventSubs[evt] ?? [];
152
- const idx = list.indexOf(callback);
153
- if (idx >= 0) list.splice(idx, 1);
154
- this.eventSubs[evt] = list;
155
- }
156
-
157
- private static eventSubs: { [key: string]: Function[] } = {};
158
-
159
- public webXR?: WebXR;
160
- public index: number = -1;
161
- public controllerModel!: XRControllerModel;
162
- public controller!: Group;
163
- public controllerGrip!: Group;
164
- public hand!: Group;
165
- public handPointerModel!: OculusHandPointerModel;
166
- public grabbed: AttachedObject | null = null;
167
- public input: XRInputSource | null = null;
168
- public type: ControllerType = ControllerType.PhysicalDevice;
169
- public showRaycastLine: boolean = true;
170
-
171
- get isUsingHands(): boolean {
172
- const r = this.input?.hand;
173
- return r !== null && r !== undefined;
174
- }
175
-
176
- get wrist(): Object3D | null {
177
- if (!this.hand) return null;
178
- const jnts = this.hand["joints"];
179
- if (!jnts) return null;
180
- return jnts["wrist"];
181
- }
182
-
183
- private _wristQuaternion: Quaternion | null = null;
184
- getWristQuaternion(): Quaternion | null {
185
- const wrist = this.wrist;
186
- if (!wrist) return null;
187
- if (!this._wristQuaternion) this._wristQuaternion = new Quaternion();
188
- const wr = getWorldQuaternion(wrist).multiply(this._wristQuaternion.setFromEuler(new Euler(-Math.PI / 4, 0, 0)));
189
- return wr;
190
- }
191
-
192
- private movementVector: Vector3 = new Vector3();
193
- private worldRot: Quaternion = new Quaternion();
194
- private joystick: Vector2 = new Vector2();
195
- private didRotate: boolean = false;
196
- private didTeleport: boolean = false;
197
- private didChangeScale: boolean = false;
198
- private static PreviousCameraFarDistance: number | undefined = undefined;
199
- private static MovementSpeedFactor: number = 1;
200
-
201
- private lastHit: Intersection | null = null;
202
-
203
- private raycastLine: Line | null = null;
204
- private _raycastHitPoint: Object3D | null = null;
205
- private _connnectedCallback: any | null = null;
206
- private _disconnectedCallback: any | null = null;
207
- private _selectStartEvt: any | null = null;
208
- private _selectEndEvt: any | null = null;
209
-
210
- public get selectionDown(): boolean { return this._selectionPressed && !this._selectionPressedLastFrame; }
211
- public get selectionUp(): boolean { return !this._selectionPressed && this._selectionPressedLastFrame; }
212
- public get selectionPressed(): boolean { return this._selectionPressed; }
213
- public get selectionClick(): boolean { return this._selectionEndTime - this._selectionStartTime < 0.3; }
214
- public get raycastHitPoint(): Object3D | null { return this._raycastHitPoint; }
215
-
216
- private _selectionPressed: boolean = false;
217
- private _selectionPressedLastFrame: boolean = false;
218
- private _selectionStartTime: number = 0;
219
- private _selectionEndTime: number = 0;
220
-
221
- public get useSmoothing(): boolean { return this._useSmoothing };
222
- private _useSmoothing: boolean = true;
223
-
224
- awake(): void {
225
- if (!this.controller) {
226
- console.warn("WebXRController: Missing controller object.", this);
227
- return;
228
- }
229
- this._connnectedCallback = this.onSourceConnected.bind(this);
230
- this._disconnectedCallback = this.onSourceDisconnected.bind(this);
231
- this._selectStartEvt = this.onSelectStart.bind(this);
232
- this._selectEndEvt = this.onSelectEnd.bind(this);
233
- if (this.type === ControllerType.Touch) {
234
- this.controllerGrip.addEventListener("connected", this._connnectedCallback);
235
- this.controllerGrip.addEventListener("disconnected", this._disconnectedCallback);
236
- this.controller.addEventListener('selectstart', this._selectStartEvt);
237
- this.controller.addEventListener('selectend', this._selectEndEvt);
238
- }
239
- if (this.type === ControllerType.PhysicalDevice) {
240
- this.controller.addEventListener('selectstart', this._selectStartEvt);
241
- this.controller.addEventListener('selectend', this._selectEndEvt);
242
- }
243
- }
244
-
245
- onDestroy(): void {
246
- if (this.type === ControllerType.Touch) {
247
- this.controllerGrip.removeEventListener("connected", this._connnectedCallback);
248
- this.controllerGrip.removeEventListener("disconnected", this._disconnectedCallback);
249
- this.controller.removeEventListener('selectstart', this._selectStartEvt);
250
- this.controller.removeEventListener('selectend', this._selectEndEvt);
251
- }
252
- if (this.type === ControllerType.PhysicalDevice) {
253
- this.controller.removeEventListener('selectstart', this._selectStartEvt);
254
- this.controller.removeEventListener('selectend', this._selectEndEvt);
255
- }
256
-
257
- this.hand?.clear();
258
- this.controllerGrip?.clear();
259
- this.controller?.clear();
260
- }
261
-
262
- public onEnable(): void {
263
- if (!this.webXR) {
264
- console.warn("No WebXR component assigned to WebXRController.");
265
- return;
266
- }
267
-
268
- if (this.hand)
269
- this.hand.name = "Hand";
270
- if (this.controllerGrip)
271
- this.controllerGrip.name = "ControllerGrip";
272
- if (this.controller)
273
- this.controller.name = "Controller";
274
- if (this.raycastLine)
275
- this.raycastLine.name = "RaycastLine;"
276
-
277
- if (this.webXR.Controllers.indexOf(this) < 0)
278
- this.webXR.Controllers.push(this);
279
-
280
- if (!this.raycastLine)
281
- this.raycastLine = WebXRController.CreateRaycastLine();
282
- if (!this._raycastHitPoint)
283
- this._raycastHitPoint = WebXRController.CreateRaycastHitPoint();
284
-
285
- this.webXR.Rig?.add(this.hand);
286
- this.webXR.Rig?.add(this.controllerGrip);
287
- this.webXR.Rig?.add(this.controller);
288
- this.webXR.Rig?.add(this.raycastLine);
289
- this.raycastLine?.add(this._raycastHitPoint);
290
- this._raycastHitPoint.visible = false;
291
- this.hand.add(this.handPointerModel);
292
- if (debug)
293
- console.log("ADDED TO RIG", this.webXR.Rig);
294
-
295
- // // console.log("enable", this.index, this.controllerGrip.uuid)
296
- }
297
-
298
- onDisable(): void {
299
- // console.log("XR controller disabled", this);
300
- this.hand?.removeFromParent();
301
- this.controllerGrip?.removeFromParent();
302
- this.controller?.removeFromParent();
303
- this.raycastLine?.removeFromParent();
304
- this._raycastHitPoint?.removeFromParent();
305
- // console.log("Disable", this._connnectedCallback, this._disconnectedCallback);
306
- // this.controllerGrip.removeEventListener("connected", this._connnectedCallback);
307
- // this.controllerGrip.removeEventListener("disconnected", this._disconnectedCallback);
308
-
309
- if (this.webXR) {
310
- const i = this.webXR.Controllers.indexOf(this);
311
- if (i >= 0)
312
- this.webXR.Controllers.splice(i, 1);
313
- }
314
- }
315
-
316
- // onDestroy(): void {
317
- // console.log("destroyed", this.index);
318
- // }
319
-
320
- private _isConnected: boolean = false;
321
-
322
- private onSourceConnected(e: { data: XRInputSource, target: any }) {
323
- if (this._isConnected) {
324
- console.warn("Received connected event for controller that is already connected", this.index, e);
325
- return;
326
- }
327
- this._isConnected = true;
328
- this.input = e.data;
329
-
330
- if (this.type === ControllerType.Touch) {
331
- this.onSelectStart();
332
- }
333
- }
334
-
335
- private onSourceDisconnected(_e: any) {
336
- if (!this._isConnected) {
337
- console.warn("Received discnnected event for controller that is not connected", _e);
338
- return;
339
- }
340
- this._isConnected = false;
341
- if (this.type === ControllerType.Touch) {
342
- this.onSelectEnd();
343
- }
344
- this.input = null;
345
- }
346
-
347
- rayRotation: Quaternion = new Quaternion();
348
-
349
- private raycastUpdate(raycastLine: Line, wp: Vector3) {
350
- const allowRaycastLineVisible = this.showRaycastLine && this.type !== ControllerType.Touch;
351
- if (this.type === ControllerType.Touch) {
352
- raycastLine.visible = false;
353
- }
354
- else if (this.isUsingHands) {
355
- raycastLine.visible = !this.grabbed && allowRaycastLineVisible;
356
- setWorldPosition(raycastLine, wp);
357
- const jnts = this.hand!['joints'];
358
- if (jnts) {
359
- const wrist = jnts['wrist'];
360
- if (wrist && this.grabbed && this.grabbed.isCloseGrab) {
361
- const wr = this.getWristQuaternion();
362
- if (wr)
363
- this.rayRotation.copy(wr);
364
- // this.rayRotation.slerp(wr, this.useSmoothing ? t * 2 : 1);
365
- }
366
- }
367
- setWorldQuaternion(raycastLine, this.rayRotation);
368
- }
369
- else {
370
- raycastLine.visible = allowRaycastLineVisible;
371
- setWorldQuaternion(raycastLine, this.rayRotation);
372
- setWorldPosition(raycastLine, wp);
373
- }
374
- }
375
-
376
- update(): void {
377
- if (!this.webXR) return;
378
-
379
- // TODO: we should wait until we actually have models, this is just a workaround
380
- if (this.context.time.frameCount % 60 === 0) {
381
- this.setControllerLayers(this.controller, 2);
382
- this.setControllerLayers(this.controllerGrip, 2);
383
- this.setControllerLayers(this.hand, 2);
384
- }
385
-
386
- const subs = WebXRController.eventSubs[ControllerEvents.Update];
387
- if (subs && subs.length > 0) {
388
- for (const sub of subs) {
389
- sub(this);
390
- }
391
- }
392
-
393
- let t = 1;
394
- if (this.type === ControllerType.PhysicalDevice) t = this.context.time.deltaTime / .1;
395
- else if (this.isUsingHands && this.handPointerModel.pinched) t = this.context.time.deltaTime / .3;
396
- this.rayRotation.slerp(getWorldQuaternion(this.controller), this.useSmoothing ? t : 1.0);
397
- const wp = getWorldPosition(this.controller);
398
-
399
- // hide hand pointer model, it's giant and doesn't really help
400
- if (this.isUsingHands && this.handPointerModel.cursorObject) {
401
- this.handPointerModel.cursorObject.visible = false;
402
- }
403
-
404
- if (this.raycastLine) {
405
- this.raycastUpdate(this.raycastLine, wp);
406
- }
407
-
408
- this.lastHit = this.updateLastHit();
409
-
410
- if (this.grabbed) {
411
- this.grabbed.update();
412
- }
413
-
414
- this._selectionPressedLastFrame = this._selectionPressed;
415
-
416
- if (this.selectStartCallback) {
417
- this.selectStartCallback();
418
- }
419
- }
420
-
421
- onUpdate(session: XRSession) {
422
- this.lastHit = null;
423
-
424
- if (!session || session.inputSources.length <= this.index) {
425
- this.input = null;
426
- return;
427
- }
428
- if (this.type === ControllerType.PhysicalDevice)
429
- this.input = session.inputSources[this.index];
430
- if (!this.input) return;
431
- const rig = this.webXR!.Rig;
432
- if (!rig) return;
433
-
434
- if (this._didNotEndSelection && !this.handPointerModel.pinched) {
435
- this._didNotEndSelection = false;
436
- this.onSelectEnd();
437
- }
438
-
439
- this.updateStick(this.input);
440
-
441
- const buttons = this.input?.gamepad?.buttons;
442
-
443
- switch (this.input.handedness) {
444
- case "left":
445
- this.movementUpdate(rig, buttons);
446
- break;
447
-
448
- case "right":
449
- this.rotationUpdate(rig, buttons);
450
- break;
451
- }
452
- }
453
-
454
-
455
- private movementUpdate(rig: Object3D, buttons?: readonly GamepadButton[]) {
456
- const speedFactor = 3 * WebXRController.MovementSpeedFactor;
457
- const powFactor = 2;
458
- const speed = Mathf.clamp01(this.joystick.length() * 2);
459
-
460
- const sideDir = this.joystick.x > 0 ? 1 : -1;
461
- let side = Math.pow(this.joystick.x, powFactor);
462
- side *= sideDir;
463
- side *= speed;
464
-
465
-
466
- const forwardDir = this.joystick.y > 0 ? 1 : -1;
467
- let forward = Math.pow(this.joystick.y, powFactor);
468
- forward *= forwardDir;
469
- side *= speed;
470
-
471
- rig.getWorldQuaternion(this.worldRot);
472
- this.movementVector.set(side, 0, forward);
473
- this.movementVector.applyQuaternion(this.webXR!.TransformOrientation);
474
- this.movementVector.y = 0;
475
- this.movementVector.applyQuaternion(this.worldRot);
476
- this.movementVector.multiplyScalar(speedFactor * this.context.time.deltaTime);
477
- rig.position.add(this.movementVector);
478
-
479
- if (this.isUsingHands)
480
- this.runTeleport(rig, buttons);
481
- }
482
-
483
- private rotationUpdate(rig: Object3D, buttons?: readonly GamepadButton[]) {
484
- const rotate = this.joystick.x;
485
- const rotAbs = Math.abs(rotate);
486
- if (rotAbs < 0.4) {
487
- this.didRotate = false;
488
- }
489
- else if (rotAbs > .5 && !this.didRotate) {
490
- const dir = rotate > 0 ? -1 : 1;
491
- rig.rotateY(Mathf.toRadians(30 * dir));
492
- this.didRotate = true;
493
- }
494
-
495
- this.runTeleport(rig, buttons);
496
- }
497
- private _pinchStartTime: number | undefined = undefined;
498
-
499
- private runTeleport(rig: Object3D, buttons?: readonly GamepadButton[]) {
500
- let teleport = -this.joystick.y;
501
- if (this.hand?.visible && !this.grabbed) {
502
- const pinched = this.handPointerModel.isPinched();
503
- if (pinched && this._pinchStartTime === undefined) {
504
- this._pinchStartTime = this.context.time.time;
505
- }
506
- if (pinched && this._pinchStartTime && this.context.time.time - this._pinchStartTime > .8) {
507
- // hacky approach for basic hand teleportation -
508
- // we teleport if we pinch and the back of the hand points down (open hand gesture)
509
- // const v1 = new Vector3();
510
- // const worldQuaternion = new Quaternion();
511
- // this.controller.getWorldQuaternion(worldQuaternion);
512
- // v1.copy(this.controller.up).applyQuaternion(worldQuaternion);
513
- // const dotPr = -v1.dot(this.controller.up);
514
- teleport = this.handPointerModel.isPinched() ? 1 : 0;
515
- }
516
- if (!pinched) this._pinchStartTime = undefined;
517
- }
518
- else this._pinchStartTime = undefined;
519
-
520
- const inVR = this.webXR!.IsInVR;
521
- const xrRig = this.webXR!.Rig;
522
- let doTeleport = teleport > .5 && inVR;
523
- let isInMiniatureMode = xrRig ? xrRig?.scale?.x < .999 : false;
524
- let newRigScale: number | null = null;
525
-
526
- if (buttons && this.input && !this.input.hand) {
527
- for (let i = 0; i < buttons.length; i++) {
528
- const btn = buttons[i];
529
- // button[4] seems to be the A button if it exists. On hololens it's randomly pressed though for hands
530
- // see https://www.w3.org/TR/webxr-gamepads-module-1/#xr-standard-gamepad-mapping
531
- if (i === 4) {
532
- if (btn.pressed && !this.didChangeScale && inVR) {
533
- this.didChangeScale = true;
534
- const rig = xrRig;
535
- if (rig) {
536
- const args = this.switchScale(rig, doTeleport, isInMiniatureMode, newRigScale);
537
- doTeleport = args.doTeleport;
538
- isInMiniatureMode = args.isInMiniatureMode;
539
- newRigScale = args.newRigScale;
540
- }
541
- }
542
- else if (!btn.pressed)
543
- this.didChangeScale = false;
544
- }
545
- }
546
- }
547
-
548
- if (doTeleport) {
549
- if (!this.didTeleport) {
550
- const rc = this.raycast();
551
- this.didTeleport = true;
552
- if (rc && rc.length > 0) {
553
- const hit = rc[0];
554
- if (isInMiniatureMode || this.isValidTeleportTarget(hit.object)) {
555
- const point = hit.point;
556
- setWorldPosition(rig, point);
557
- }
558
- }
559
- }
560
- }
561
- else if (teleport < .1) {
562
- this.didTeleport = false;
563
- }
564
-
565
- if (newRigScale !== null) {
566
- rig.scale.set(newRigScale, newRigScale, newRigScale);
567
- rig.updateMatrixWorld();
568
- }
569
- }
570
-
571
-
572
- private isValidTeleportTarget(obj: Object3D): boolean {
573
- return GameObject.getComponentInParent(obj, TeleportTarget) != null;
574
- }
575
-
576
- private switchScale(rig: Object3D, doTeleport: boolean, isInMiniatureMode: boolean, newRigScale: number | null) {
577
- if (!isInMiniatureMode) {
578
- isInMiniatureMode = true;
579
- doTeleport = true;
580
- newRigScale = .1;
581
- WebXRController.MovementSpeedFactor = newRigScale * 2;
582
- const cam = this.context.mainCamera as PerspectiveCamera;
583
- WebXRController.PreviousCameraFarDistance = cam.far;
584
- cam.far /= newRigScale;
585
- }
586
- else {
587
- isInMiniatureMode = false;
588
- rig.scale.set(1, 1, 1);
589
- newRigScale = 1;
590
- WebXRController.MovementSpeedFactor = 1;
591
- const cam = this.context.mainCamera as PerspectiveCamera;
592
- if (WebXRController.PreviousCameraFarDistance)
593
- cam.far = WebXRController.PreviousCameraFarDistance;
594
- }
595
- return { doTeleport, isInMiniatureMode, newRigScale }
596
- }
597
-
598
- private updateStick(inputSource: XRInputSource) {
599
- if (!inputSource || !inputSource.gamepad || inputSource.gamepad.axes?.length < 4) return;
600
- this.joystick.x = inputSource.gamepad.axes[2];
601
- this.joystick.y = inputSource.gamepad.axes[3];
602
- }
603
-
604
- private updateLastHit(): Intersection | null {
605
- const rc = this.raycast();
606
- const hit = rc ? rc[0] : null;
607
- this.lastHit = hit;
608
- let factor = 1;
609
- if (this.webXR!.Rig) {
610
- factor /= this.webXR!.Rig.scale.x;
611
- }
612
- // if (!hit) factor = 0;
613
-
614
- if (this.raycastLine) {
615
- this.raycastLine.scale.z = factor * (this.lastHit?.distance ?? 9999);
616
- const mat = this.raycastLine.material as LineBasicMaterial;
617
- if (hit != null) mat.color = WebXRController.raycastColor;
618
- else mat.color = WebXRController.raycastNoHitColor;
619
- }
620
- if (this._raycastHitPoint) {
621
- if (this.lastHit != null) {
622
- this._raycastHitPoint.position.z = -1;// -this.lastHit.distance;
623
- const scale = Mathf.clamp(this.lastHit.distance * .01 * factor, .015, .1);
624
- this._raycastHitPoint.scale.set(scale, scale, scale);
625
- }
626
- this._raycastHitPoint.visible = this.lastHit !== null && this.lastHit !== undefined;
627
- }
628
- return hit;
629
- }
630
-
631
- private onSelectStart() {
632
- if (!this.context.connection.allowEditing) return;
633
- // console.log("SELECT START", _event);
634
- // if we process the event immediately the controller
635
- // world positions are not yet correctly updated and we have info from the last frame
636
- // so we delay the event processing one frame
637
- // only necessary for AR - ideally we can get it to work right here
638
- // but should be fine as a workaround for now
639
- this.selectStartCallback = () => this.onHandleSelectStart();
640
- }
641
-
642
- private selectStartCallback: Function | null = null;
643
- private lastSelectStartObject: Object3D | null = null;;
644
-
645
- private onHandleSelectStart() {
646
- this.selectStartCallback = null;
647
- this._selectionPressed = true;
648
- this._selectionStartTime = this.context.time.time;
649
- this._selectionEndTime = 1000;
650
- // console.log("DOWN", this.index, WebXRController.eventSubs);
651
-
652
- // let maxDistance = this.isUsingHands ? .1 : undefined;
653
- let intersections: Intersection[] | null = null;
654
- let closeGrab: boolean = false;
655
- if (this.isUsingHands) {
656
- intersections = this.overlap();
657
- if (intersections.length <= 0) {
658
- intersections = this.raycast();
659
- closeGrab = false;
660
- }
661
- else {
662
- closeGrab = true;
663
- }
664
- }
665
- else intersections = this.raycast();
666
-
667
- if (debug)
668
- console.log("onHandleSelectStart", "close grab? " + closeGrab, "intersections", intersections);
669
-
670
- if (intersections && intersections.length > 0) {
671
- for (const intersection of intersections) {
672
- const object = intersection.object;
673
- this.lastSelectStartObject = object;
674
- const args = { selected: object, grab: object };
675
- const subs = WebXRController.eventSubs[ControllerEvents.SelectStart];
676
- if (subs && subs.length > 0) {
677
- for (const sub of subs) {
678
- sub(this, args);
679
- }
680
- }
681
- if (args.grab !== object && debug)
682
- console.log("Grabbed object changed", "original", object, "new", args.grab);
683
- if (args.grab) {
684
- this.grabbed = AttachedObject.TryTake(this, args.grab, intersection, closeGrab);
685
- }
686
- break;
687
- }
688
- }
689
- else {
690
- const subs = WebXRController.eventSubs[ControllerEvents.SelectStart];
691
- const args = { selected: null, grab: null };
692
- if (subs && subs.length > 0) {
693
- for (const sub of subs) {
694
- sub(this, args);
695
- }
696
- }
697
- }
698
- }
699
-
700
- private _didNotEndSelection: boolean = false;
701
-
702
- private onSelectEnd() {
703
- if (this.isUsingHands) {
704
- if (this.handPointerModel.pinched) {
705
- this._didNotEndSelection = true;
706
- return;
707
- }
708
- }
709
-
710
- if (!this._selectionPressed) return;
711
- this.selectStartCallback = null;
712
- this._selectionPressed = false;
713
- this._selectionEndTime = this.context.time.time;
714
-
715
- const args = { grab: this.grabbed?.selected ?? this.lastSelectStartObject };
716
- const subs = WebXRController.eventSubs[ControllerEvents.SelectEnd];
717
- if (subs && subs.length > 0) {
718
- for (const sub of subs) {
719
- sub(this, args);
720
- }
721
- }
722
-
723
- if (this.grabbed) {
724
- this.grabbed.free();
725
- this.grabbed = null;
726
- }
727
- }
728
-
729
- private testIsVisible(obj: Object3D | null): boolean {
730
- if (!obj) return false;
731
- if (GameObject.isActiveInHierarchy(obj) === false) return false;
732
- if (UIRaycastUtils.isInteractable(obj) === false) {
733
- return false;
734
- }
735
- return true;
736
- // if (!obj.visible) return false;
737
- // return this.testIsVisible(obj.parent);
738
- }
739
-
740
- private setControllerLayers(obj: Object3D, layer: number) {
741
- if (!obj) return;
742
- obj.layers.set(layer);
743
- if (obj.children) {
744
- for (const ch of obj.children) {
745
- if (this.grabbed?.selected === ch || this.grabbed?.selectedMesh === ch) {
746
- continue;
747
- }
748
- this.setControllerLayers(ch, layer);
749
- }
750
- }
751
- }
752
-
753
- public getRay(): Ray {
754
- const ray = new Ray();
755
- // this.tempMatrix.identity().extractRotation(this.controller.matrixWorld);
756
- // ray.origin.setFromMatrixPosition(this.controller.matrixWorld);
757
- ray.origin.copy(getWorldPosition(this.controller));
758
- ray.direction.set(0, 0, -1).applyQuaternion(this.rayRotation);
759
- return ray;
760
- }
761
-
762
- private closeGrabBoundingBoxHelper?: BoxHelper;
763
-
764
- public overlap(): Intersection[] {
765
- const overlapCenter = (this.isUsingHands && this.handPointerModel) ? this.handPointerModel.pointerObject : this.controllerGrip;
766
-
767
- if (debug) {
768
- if (!this.closeGrabBoundingBoxHelper && overlapCenter) {
769
- this.closeGrabBoundingBoxHelper = new BoxHelper(overlapCenter, 0xffff00);
770
- this.scene.add(this.closeGrabBoundingBoxHelper);
771
- }
772
-
773
- if (this.closeGrabBoundingBoxHelper && overlapCenter) {
774
- this.closeGrabBoundingBoxHelper.setFromObject(overlapCenter);
775
- }
776
- }
777
-
778
- if (!overlapCenter)
779
- return new Array<Intersection>();
780
-
781
- const wp = getWorldPosition(overlapCenter).clone();
782
- return this.context.physics.sphereOverlap(wp, .02);
783
- }
784
-
785
- public raycast(): Intersection[] {
786
- const opts = new RaycastOptions();
787
- opts.layerMask = new Layers();
788
- opts.layerMask.enableAll();
789
- opts.layerMask.disable(2);
790
- opts.ray = this.getRay();
791
- const hits = this.context.physics.raycast(opts);
792
- for (let i = 0; i < hits.length; i++) {
793
- const hit = hits[i];
794
- const obj = hit.object;
795
- if (!this.testIsVisible(obj)) {
796
- hits.splice(i, 1);
797
- i--;
798
- continue;
799
- }
800
- hit.object = UIRaycastUtils.getObject(obj);
801
- break;
802
- }
803
- // console.log(...hits);
804
- return hits;
805
- }
806
- }
807
-
808
-
809
- export enum AttachedObjectEvents {
810
- WillTake = "WillTake",
811
- DidTake = "DidTake",
812
- WillFree = "WillFree",
813
- DidFree = "DidFree",
814
- }
815
-
816
- export class AttachedObject {
817
-
818
- public static Events: { [key: string]: Function[] } = {};
819
- public static AddEventListener(event: AttachedObjectEvents, callback: Function): Function {
820
- if (!AttachedObject.Events[event]) AttachedObject.Events[event] = [];
821
- AttachedObject.Events[event].push(callback);
822
- return callback;
823
- }
824
- public static RemoveEventListener(event: AttachedObjectEvents, callback: Function | null) {
825
- if (!callback) return;
826
- if (!AttachedObject.Events[event]) return;
827
- const idx = AttachedObject.Events[event].indexOf(callback);
828
- if (idx >= 0) AttachedObject.Events[event].splice(idx, 1);
829
- }
830
-
831
-
832
- public static Current: AttachedObject[] = [];
833
-
834
- private static Register(obj: AttachedObject) {
835
-
836
- if (!this.Current.find(x => x === obj)) {
837
- this.Current.push(obj);
838
- }
839
- }
840
-
841
- private static Remove(obj: AttachedObject) {
842
- const i = this.Current.indexOf(obj);
843
- if (i >= 0) {
844
- this.Current.splice(i, 1);
845
- }
846
- }
847
-
848
- public static TryTake(controller: WebXRController, candidate: Object3D, intersection: Intersection, closeGrab: boolean): AttachedObject | null {
849
- const interactable = GameObject.getComponentInParent(candidate, Interactable);
850
- if (!interactable) {
851
- if (debug)
852
- console.warn("Prevented taking object that is not interactable", candidate);
853
- return null;
854
- }
855
- else candidate = interactable.gameObject;
856
-
857
-
858
- let objectToAttach = candidate;
859
- const sync = GameObject.getComponentInParent(candidate, SyncedTransform);
860
- if (sync) {
861
- sync.requestOwnership();
862
- objectToAttach = sync.gameObject;
863
- }
864
-
865
- for (const o of this.Current) {
866
- if (o.selected === objectToAttach) {
867
- if (o.controller === controller) return o;
868
- o.free();
869
- o.Take(controller, objectToAttach, candidate, sync, interactable, intersection, closeGrab);
870
- return o;
871
- }
872
- }
873
-
874
- const att = new AttachedObject();
875
- att.Take(controller, objectToAttach, candidate, sync, interactable, intersection, closeGrab);
876
- return att;
877
- }
878
-
879
-
880
- public sync: SyncedTransform | null = null;
881
- public selected: Object3D | null = null;
882
- public selectedParent: Object3D | null = null;
883
- public selectedMesh: Mesh | null = null;
884
- public controller: WebXRController | null = null;
885
- public grabTime: number = 0;
886
- public grabUUID: string = "";
887
- public isCloseGrab: boolean = false; // when taken via sphere cast with hands
888
-
889
- private originalMaterial: Material | Material[] | null = null;
890
- private usageMarker: UsageMarker | null = null;
891
- private rigidbodies: Rigidbody[] | null = null;
892
- private didReparent: boolean = false;
893
- private grabDistance: number = 0;
894
- private interactable: Interactable | null = null;
895
- private positionSource: Object3D | null = null;
896
-
897
- private Take(controller: WebXRController, take: Object3D, hit: Object3D, sync: SyncedTransform | null, _interactable: Interactable,
898
- intersection: Intersection, closeGrab: boolean)
899
- : AttachedObject {
900
- console.assert(take !== null, "Expected object to be taken but was", take);
901
-
902
- if (controller.isUsingHands) {
903
- this.positionSource = closeGrab ? controller.wrist : controller.controller;
904
- }
905
- else {
906
- this.positionSource = controller.controller;
907
- }
908
- if (!this.positionSource) {
909
- console.warn("No position source");
910
- return this;
911
- }
912
-
913
- const args = { controller, take, hit, sync, interactable: _interactable };
914
- AttachedObject.Events.WillTake?.forEach(x => x(this, args));
915
-
916
-
917
- const mesh = hit as Mesh;
918
- if (mesh?.material) {
919
- this.originalMaterial = mesh.material;
920
- if (!Array.isArray(mesh.material)) {
921
- mesh.material = (mesh.material as Material).clone();
922
- if (mesh.material && mesh.material["emissive"])
923
- mesh.material["emissive"].b = .2;
924
- }
925
- }
926
-
927
- this.selected = take;
928
- if (!this.selectedParent) {
929
- this.selectedParent = take.parent;
930
- }
931
- this.selectedMesh = mesh;
932
- this.controller = controller;
933
- this.interactable = _interactable;
934
- this.isCloseGrab = closeGrab;
935
- // if (interactable.canGrab) {
936
- // this.didReparent = true;
937
- // this.device.controller.attach(take);
938
- // }
939
- // else
940
- this.didReparent = false;
941
-
942
-
943
- this.sync = sync;
944
- this.grabTime = controller.context.time.time;
945
- this.grabUUID = Date.now().toString();
946
- this.usageMarker = GameObject.addNewComponent(this.selected, UsageMarker);
947
- this.rigidbodies = GameObject.getComponentsInChildren(this.selected, Rigidbody);
948
- getWorldPosition(this.positionSource, this.lastControllerWorldPos);
949
- const getGrabPoint = () => closeGrab ? this.lastControllerWorldPos.clone() : intersection.point.clone();
950
- this.grabDistance = getGrabPoint().distanceTo(this.lastControllerWorldPos);
951
- this.totalChangeAlongDirection = 0.0;
952
-
953
- // we're storing position relative to the grab point
954
- // we're storing rotation relative to the ray
955
- this.localPositionOffsetToGrab = this.selected.worldToLocal(getGrabPoint());
956
- const rot = controller.isUsingHands && closeGrab ? this.controller.getWristQuaternion()!.clone() : controller.rayRotation.clone();
957
- getWorldQuaternion(this.selected, this.localQuaternionToGrab).premultiply(rot.invert());
958
-
959
- const rig = this.controller.webXR!.Rig;
960
- if (rig)
961
- this.rigPositionLastFrame.copy(getWorldPosition(rig))
962
-
963
- Avatar_POI.Add(controller.context, this.selected);
964
- AttachedObject.Register(this);
965
-
966
- if (this.sync) {
967
- this.sync.fastMode = true;
968
- }
969
-
970
- AttachedObject.Events.DidTake?.forEach(x => x(this, args));
971
-
972
- return this;
973
- }
974
-
975
- public free(): void {
976
- if (!this.selected) return;
977
-
978
- const args = { controller: this.controller, take: this.selected, hit: this.selected, sync: this.sync, interactable: null };
979
- AttachedObject.Events.WillFree?.forEach(x => x(this, args));
980
-
981
- Avatar_POI.Remove(this.controller!.context, this.selected);
982
- AttachedObject.Remove(this);
983
-
984
- if (this.sync) {
985
- this.sync.fastMode = false;
986
- }
987
-
988
- const mesh = this.selectedMesh;
989
- if (mesh && this.originalMaterial && mesh.material) {
990
- mesh.material = this.originalMaterial;
991
- }
992
-
993
- const object = this.selected;
994
- // only attach the object back if it has a parent
995
- // no parent means it was destroyed while holding it!
996
- if (this.didReparent && object.parent) {
997
- const prevParent = this.selectedParent;
998
- if (prevParent) prevParent.attach(object);
999
- else this.controller?.context.scene.attach(object);
1000
- }
1001
-
1002
- this.usageMarker?.destroy();
1003
-
1004
- if (this.controller)
1005
- this.controller.grabbed = null;
1006
- this.selected = null;
1007
- this.selectedParent = null;
1008
- this.selectedMesh = null;
1009
- this.sync = null;
1010
-
1011
-
1012
- // TODO: make throwing work again
1013
- if (this.rigidbodies) {
1014
- for (const rb of this.rigidbodies) {
1015
- rb.wakeUp();
1016
- rb.setVelocity(rb.smoothedVelocity);
1017
- }
1018
- }
1019
- this.rigidbodies = null;
1020
-
1021
- this.localPositionOffsetToGrab = null;
1022
- this.quaternionLerp = null;
1023
-
1024
- AttachedObject.Events.DidFree?.forEach(x => x(this, args));
1025
- }
1026
-
1027
- public grabPoint: Vector3 = new Vector3();
1028
-
1029
- private localPositionOffsetToGrab: Vector3 | null = null;
1030
- private localPositionOffsetToGrab_worldSpace: Vector3 = new Vector3();
1031
- private localQuaternionToGrab: Quaternion = new Quaternion(0, 0, 0, 1);
1032
- private targetDir: Vector3 | null = null;
1033
- private quaternionLerp: Quaternion | null = null;
1034
-
1035
- private controllerDir = new Vector3();
1036
- private controllerWorldPos = new Vector3();
1037
- private lastControllerWorldPos = new Vector3();
1038
- private controllerPosDelta = new Vector3();
1039
- private totalChangeAlongDirection = 0.0;
1040
- private rigPositionLastFrame = new Vector3();
1041
-
1042
- private controllerMovementSinceLastFrame() {
1043
- if (!this.positionSource || !this.controller) return 0.0;
1044
-
1045
- // controller direction
1046
- this.controllerDir.set(0, 0, -1);
1047
- this.controllerDir.applyQuaternion(this.controller.rayRotation);
1048
-
1049
- // controller delta
1050
- getWorldPosition(this.positionSource, this.controllerWorldPos);
1051
- this.controllerPosDelta.copy(this.controllerWorldPos);
1052
- this.controllerPosDelta.sub(this.lastControllerWorldPos);
1053
- this.lastControllerWorldPos.copy(this.controllerWorldPos);
1054
- const rig = this.controller.webXR!.Rig;
1055
- if (rig) {
1056
- const rigPos = getWorldPosition(rig);
1057
- const rigDelta = this.rigPositionLastFrame.sub(rigPos);
1058
- this.controllerPosDelta.add(rigDelta);
1059
- this.rigPositionLastFrame.copy(rigPos);
1060
- }
1061
-
1062
- // calculate delta along direction
1063
- const changeAlongControllerDirection = this.controllerDir.dot(this.controllerPosDelta);
1064
-
1065
- return changeAlongControllerDirection;
1066
- }
1067
-
1068
- public update() {
1069
- if (this.rigidbodies)
1070
- for (const rb of this.rigidbodies)
1071
- rb.resetVelocities();
1072
- // TODO: add/use sync lost ownership event
1073
- if (this.sync && this.controller && this.controller.context.connection.isInRoom) {
1074
- const td = this.controller.context.time.time - this.grabTime;
1075
- // if (time.frameCount % 60 === 0) {
1076
- // console.log("ownership?", this.selected.name, this.sync.hasOwnership(), td)
1077
- // }
1078
- if (td > 3) {
1079
- // if (time.frameCount % 60 === 0) {
1080
- // console.log(this.sync.hasOwnership())
1081
- // }
1082
- if (this.sync.hasOwnership() === false) {
1083
- console.log("no ownership, will leave", this.sync.guid);
1084
- this.free();
1085
- }
1086
- }
1087
- }
1088
- if (this.interactable && !this.interactable.canGrab) return;
1089
-
1090
- if (!this.didReparent && this.selected && this.controller) {
1091
-
1092
- const rigScale = this.controller.webXR!.Rig?.scale.x ?? 1.0;
1093
-
1094
- this.totalChangeAlongDirection += this.controllerMovementSinceLastFrame();
1095
- // console.log(this.totalChangeAlongDirection);
1096
-
1097
- // alert("yo: " + this.controller.webXR.Rig?.scale.x); // this is 0.1 on Hololens
1098
- let currentDist = 1.0;
1099
- if (this.controller.type === ControllerType.PhysicalDevice) // only for controllers and not on touch (AR touches are controllers)
1100
- {
1101
- currentDist = Math.max(0.0, 1 + this.totalChangeAlongDirection * 2.0 / rigScale);
1102
- currentDist = currentDist * currentDist * currentDist;
1103
- }
1104
- if (this.grabDistance / rigScale < 0.8) currentDist = 1.0; // don't accelerate if this is a close grab, want full control
1105
-
1106
- if (!this.targetDir) {
1107
- this.targetDir = new Vector3();
1108
- }
1109
- this.targetDir.set(0, 0, -this.grabDistance * currentDist);
1110
- const target = this.targetDir.applyQuaternion(this.controller.rayRotation).add(this.controllerWorldPos);
1111
-
1112
- // apply rotation
1113
- const targetQuat = this.controller.rayRotation.clone().multiplyQuaternions(this.controller.rayRotation, this.localQuaternionToGrab);
1114
- if (!this.quaternionLerp) {
1115
- this.quaternionLerp = targetQuat.clone();
1116
- }
1117
- this.quaternionLerp.slerp(targetQuat, this.controller.useSmoothing ? this.controller.context.time.deltaTime / .03 : 1.0);
1118
- setWorldQuaternion(this.selected, this.quaternionLerp);
1119
- this.selected.updateWorldMatrix(false, false); // necessary so that rotation is correct for the following position update
1120
-
1121
- // apply position
1122
- this.grabPoint.copy(target);
1123
- // apply local grab offset
1124
- if (this.localPositionOffsetToGrab) {
1125
- this.localPositionOffsetToGrab_worldSpace.copy(this.localPositionOffsetToGrab);
1126
- this.selected.localToWorld(this.localPositionOffsetToGrab_worldSpace).sub(getWorldPosition(this.selected));
1127
- target.sub(this.localPositionOffsetToGrab_worldSpace);
1128
- }
1129
- setWorldPosition(this.selected, target);
1130
- }
1131
-
1132
-
1133
- if (this.rigidbodies != null) {
1134
- for (const rb of this.rigidbodies) {
1135
- rb.wakeUp();
1136
- }
1137
- }
1138
-
1139
- InstancingUtil.markDirty(this.selected, true);
1140
- }
1141
- }
1
+ import { BoxHelper, BufferGeometry, Color, Euler, Group, Intersection, Layers, Line, LineBasicMaterial, Material, Mesh, MeshBasicMaterial, Object3D, PerspectiveCamera, Quaternion, Ray, SphereGeometry, Vector2, Vector3 } from "three";
2
+ import { OculusHandModel } from 'three/examples/jsm/webxr/OculusHandModel.js';
3
+ import { OculusHandPointerModel } from 'three/examples/jsm/webxr/OculusHandPointerModel.js';
4
+ import { XRControllerModel, XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js';
5
+ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
6
+
7
+ import { InstancingUtil } from "../../engine/engine_instancing.js";
8
+ import { Mathf } from "../../engine/engine_math.js";
9
+ import { RaycastOptions } from "../../engine/engine_physics.js";
10
+ import { getWorldPosition, getWorldQuaternion, setWorldPosition, setWorldQuaternion } from "../../engine/engine_three_utils.js";
11
+ import { getParam, resolveUrl } from "../../engine/engine_utils.js";
12
+ import { addDracoAndKTX2Loaders } from "../../engine/engine_loaders.js";
13
+
14
+ import { Avatar_POI } from "../avatar/Avatar_Brain_LookAt.js";
15
+ import { Behaviour, GameObject } from "../Component.js";
16
+ import { Interactable, UsageMarker } from "../Interactable.js";
17
+ import { Rigidbody } from "../RigidBody.js";
18
+ import { SyncedTransform } from "../SyncedTransform.js";
19
+ import { UIRaycastUtils } from "../ui/RaycastUtils.js";
20
+ import { WebXR } from "./WebXR.js";
21
+ import { XRRig } from "./WebXRRig.js";
22
+
23
+ const debug = getParam("debugwebxrcontroller");
24
+
25
+ export enum ControllerType {
26
+ PhysicalDevice = 0,
27
+ Touch = 1,
28
+ }
29
+
30
+ export enum ControllerEvents {
31
+ SelectStart = "select-start",
32
+ SelectEnd = "select-end",
33
+ Update = "update",
34
+ }
35
+
36
+ export class TeleportTarget extends Behaviour {
37
+
38
+ }
39
+
40
+ export class WebXRController extends Behaviour {
41
+
42
+ public static Factory: XRControllerModelFactory = new XRControllerModelFactory();
43
+
44
+ private static raycastColor: Color = new Color(.9, .3, .3);
45
+ private static raycastNoHitColor: Color = new Color(.6, .6, .6);
46
+ private static geometry = new BufferGeometry().setFromPoints([new Vector3(0, 0, 0), new Vector3(0, 0, -1)]);
47
+ private static handModels: { [index: number]: OculusHandPointerModel } = {};
48
+
49
+ private static CreateRaycastLine(): Line {
50
+ const line = new Line(this.geometry);
51
+ const mat = line.material as LineBasicMaterial;
52
+ mat.color = this.raycastColor;
53
+ // mat.linewidth = 10;
54
+ line.layers.set(2);
55
+ line.name = 'line';
56
+ line.scale.z = 1;
57
+ return line;
58
+ }
59
+
60
+ private static CreateRaycastHitPoint(): Mesh {
61
+ const geometry = new SphereGeometry(.5, 22, 22);
62
+ const material = new MeshBasicMaterial({ color: this.raycastColor });
63
+ const sphere = new Mesh(geometry, material);
64
+ sphere.visible = false;
65
+ sphere.layers.set(2);
66
+ return sphere;
67
+ }
68
+
69
+ public static Create(owner: WebXR, index: number, addTo: GameObject, type: ControllerType): WebXRController {
70
+ const ctrl = addTo ? GameObject.addNewComponent(addTo, WebXRController, false) : new WebXRController();
71
+
72
+ ctrl.webXR = owner;
73
+ ctrl.index = index;
74
+ ctrl.type = type;
75
+
76
+ const context = owner.context;
77
+ // from https://github.com/mrdoob/js/blob/master/examples/webxr_vr_dragging.html
78
+ // controllers
79
+ ctrl.controller = context.renderer.xr.getController(index);
80
+ ctrl.controllerGrip = context.renderer.xr.getControllerGrip(index);
81
+ ctrl.controllerModel = this.Factory.createControllerModel(ctrl.controller);
82
+ ctrl.controllerGrip.add(ctrl.controllerModel);
83
+
84
+ ctrl.hand = context.renderer.xr.getHand(index);
85
+
86
+ const loader = new GLTFLoader();
87
+ addDracoAndKTX2Loaders(loader, context);
88
+ if (ctrl.webXR.handModelPath && ctrl.webXR.handModelPath !== "")
89
+ loader.setPath(resolveUrl(owner.sourceId, ctrl.webXR.handModelPath));
90
+ else
91
+ // from XRHandMeshModel.js
92
+ loader.setPath('https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles/generic-hand/');
93
+ //@ts-ignore
94
+ const hand = new OculusHandModel(ctrl.hand, loader);
95
+
96
+ ctrl.hand.add(hand);
97
+ ctrl.hand.traverse(x => x.layers.set(2));
98
+
99
+ ctrl.handPointerModel = new OculusHandPointerModel(ctrl.hand, ctrl.controller);
100
+
101
+
102
+ // TODO remove all these once https://github.com/mrdoob/js/pull/23279 lands
103
+ ctrl.controller.addEventListener('connected', (_) => {
104
+ ctrl.setControllerLayers(ctrl.controllerModel, 2);
105
+ ctrl.setControllerLayers(ctrl.controllerGrip, 2);
106
+ ctrl.setControllerLayers(ctrl.hand, 2);
107
+ setTimeout(() => {
108
+ ctrl.setControllerLayers(ctrl.controllerModel, 2);
109
+ ctrl.setControllerLayers(ctrl.controllerGrip, 2);
110
+ ctrl.setControllerLayers(ctrl.hand, 2);
111
+ }, 1000);
112
+ });
113
+
114
+ // TODO: unsubscribe! this should be moved into onenable and ondisable!
115
+ // TODO remove all these once https://github.com/mrdoob/js/pull/23279 lands
116
+ ctrl.hand.addEventListener('connected', (event) => {
117
+ const xrInputSource = event.data;
118
+ if (xrInputSource.hand) {
119
+ if (owner.Rig) owner.Rig.add(ctrl.hand);
120
+ ctrl.type = ControllerType.PhysicalDevice;
121
+ ctrl.handPointerModel.traverse(x => x.layers.set(2)); // ignore raycast
122
+ ctrl.handPointerModel.pointerObject?.traverse(x => x.layers.set(2));
123
+
124
+ // when exiting and re-entering xr the joints are not parented to the hand anymore
125
+ // this is a workaround to fix that temporarely
126
+ // see https://github.com/needle-tools/needle-tiny-playground/issues/123
127
+ const jnts = ctrl.hand["joints"];
128
+ if (jnts) {
129
+ for (const key of Object.keys(jnts)) {
130
+ const joint = jnts[key];
131
+ if (joint.parent) continue;
132
+ ctrl.hand.add(joint);
133
+ }
134
+ }
135
+ }
136
+ });
137
+
138
+ return ctrl;
139
+ }
140
+
141
+ // TODO: replace with component events
142
+ public static addEventListener(evt: ControllerEvents, callback: (controller: WebXRController, args: any) => void) {
143
+ const list = this.eventSubs[evt] ?? [];
144
+ list.push(callback);
145
+ this.eventSubs[evt] = list;
146
+ }
147
+
148
+ // TODO: replace with component events
149
+ public static removeEventListener(evt: ControllerEvents, callback: (controller: WebXRController, args: any) => void) {
150
+ if (!callback) return;
151
+ const list = this.eventSubs[evt] ?? [];
152
+ const idx = list.indexOf(callback);
153
+ if (idx >= 0) list.splice(idx, 1);
154
+ this.eventSubs[evt] = list;
155
+ }
156
+
157
+ private static eventSubs: { [key: string]: Function[] } = {};
158
+
159
+ public webXR?: WebXR;
160
+ public index: number = -1;
161
+ public controllerModel!: XRControllerModel;
162
+ public controller!: Group;
163
+ public controllerGrip!: Group;
164
+ public hand!: Group;
165
+ public handPointerModel!: OculusHandPointerModel;
166
+ public grabbed: AttachedObject | null = null;
167
+ public input: XRInputSource | null = null;
168
+ public type: ControllerType = ControllerType.PhysicalDevice;
169
+ public showRaycastLine: boolean = true;
170
+
171
+ get isUsingHands(): boolean {
172
+ const r = this.input?.hand;
173
+ return r !== null && r !== undefined;
174
+ }
175
+
176
+ get wrist(): Object3D | null {
177
+ if (!this.hand) return null;
178
+ const jnts = this.hand["joints"];
179
+ if (!jnts) return null;
180
+ return jnts["wrist"];
181
+ }
182
+
183
+ private _wristQuaternion: Quaternion | null = null;
184
+ getWristQuaternion(): Quaternion | null {
185
+ const wrist = this.wrist;
186
+ if (!wrist) return null;
187
+ if (!this._wristQuaternion) this._wristQuaternion = new Quaternion();
188
+ const wr = getWorldQuaternion(wrist).multiply(this._wristQuaternion.setFromEuler(new Euler(-Math.PI / 4, 0, 0)));
189
+ return wr;
190
+ }
191
+
192
+ private movementVector: Vector3 = new Vector3();
193
+ private worldRot: Quaternion = new Quaternion();
194
+ private joystick: Vector2 = new Vector2();
195
+ private didRotate: boolean = false;
196
+ private didTeleport: boolean = false;
197
+ private didChangeScale: boolean = false;
198
+ private static PreviousCameraFarDistance: number | undefined = undefined;
199
+ private static MovementSpeedFactor: number = 1;
200
+
201
+ private lastHit: Intersection | null = null;
202
+
203
+ private raycastLine: Line | null = null;
204
+ private _raycastHitPoint: Object3D | null = null;
205
+ private _connnectedCallback: any | null = null;
206
+ private _disconnectedCallback: any | null = null;
207
+ private _selectStartEvt: any | null = null;
208
+ private _selectEndEvt: any | null = null;
209
+
210
+ public get selectionDown(): boolean { return this._selectionPressed && !this._selectionPressedLastFrame; }
211
+ public get selectionUp(): boolean { return !this._selectionPressed && this._selectionPressedLastFrame; }
212
+ public get selectionPressed(): boolean { return this._selectionPressed; }
213
+ public get selectionClick(): boolean { return this._selectionEndTime - this._selectionStartTime < 0.3; }
214
+ public get raycastHitPoint(): Object3D | null { return this._raycastHitPoint; }
215
+
216
+ private _selectionPressed: boolean = false;
217
+ private _selectionPressedLastFrame: boolean = false;
218
+ private _selectionStartTime: number = 0;
219
+ private _selectionEndTime: number = 0;
220
+
221
+ public get useSmoothing(): boolean { return this._useSmoothing };
222
+ private _useSmoothing: boolean = true;
223
+
224
+ awake(): void {
225
+ if (!this.controller) {
226
+ console.warn("WebXRController: Missing controller object.", this);
227
+ return;
228
+ }
229
+ this._connnectedCallback = this.onSourceConnected.bind(this);
230
+ this._disconnectedCallback = this.onSourceDisconnected.bind(this);
231
+ this._selectStartEvt = this.onSelectStart.bind(this);
232
+ this._selectEndEvt = this.onSelectEnd.bind(this);
233
+ if (this.type === ControllerType.Touch) {
234
+ this.controllerGrip.addEventListener("connected", this._connnectedCallback);
235
+ this.controllerGrip.addEventListener("disconnected", this._disconnectedCallback);
236
+ this.controller.addEventListener('selectstart', this._selectStartEvt);
237
+ this.controller.addEventListener('selectend', this._selectEndEvt);
238
+ }
239
+ if (this.type === ControllerType.PhysicalDevice) {
240
+ this.controller.addEventListener('selectstart', this._selectStartEvt);
241
+ this.controller.addEventListener('selectend', this._selectEndEvt);
242
+ }
243
+ }
244
+
245
+ onDestroy(): void {
246
+ if (this.type === ControllerType.Touch) {
247
+ this.controllerGrip.removeEventListener("connected", this._connnectedCallback);
248
+ this.controllerGrip.removeEventListener("disconnected", this._disconnectedCallback);
249
+ this.controller.removeEventListener('selectstart', this._selectStartEvt);
250
+ this.controller.removeEventListener('selectend', this._selectEndEvt);
251
+ }
252
+ if (this.type === ControllerType.PhysicalDevice) {
253
+ this.controller.removeEventListener('selectstart', this._selectStartEvt);
254
+ this.controller.removeEventListener('selectend', this._selectEndEvt);
255
+ }
256
+
257
+ this.hand?.clear();
258
+ this.controllerGrip?.clear();
259
+ this.controller?.clear();
260
+ }
261
+
262
+ public onEnable(): void {
263
+ if (!this.webXR) {
264
+ console.warn("No WebXR component assigned to WebXRController.");
265
+ return;
266
+ }
267
+
268
+ if (this.hand)
269
+ this.hand.name = "Hand";
270
+ if (this.controllerGrip)
271
+ this.controllerGrip.name = "ControllerGrip";
272
+ if (this.controller)
273
+ this.controller.name = "Controller";
274
+ if (this.raycastLine)
275
+ this.raycastLine.name = "RaycastLine;"
276
+
277
+ if (this.webXR.Controllers.indexOf(this) < 0)
278
+ this.webXR.Controllers.push(this);
279
+
280
+ if (!this.raycastLine)
281
+ this.raycastLine = WebXRController.CreateRaycastLine();
282
+ if (!this._raycastHitPoint)
283
+ this._raycastHitPoint = WebXRController.CreateRaycastHitPoint();
284
+
285
+ this.webXR.Rig?.add(this.hand);
286
+ this.webXR.Rig?.add(this.controllerGrip);
287
+ this.webXR.Rig?.add(this.controller);
288
+ this.webXR.Rig?.add(this.raycastLine);
289
+ this.raycastLine?.add(this._raycastHitPoint);
290
+ this._raycastHitPoint.visible = false;
291
+ this.hand.add(this.handPointerModel);
292
+ if (debug)
293
+ console.log("ADDED TO RIG", this.webXR.Rig);
294
+
295
+ // // console.log("enable", this.index, this.controllerGrip.uuid)
296
+ }
297
+
298
+ onDisable(): void {
299
+ // console.log("XR controller disabled", this);
300
+ this.hand?.removeFromParent();
301
+ this.controllerGrip?.removeFromParent();
302
+ this.controller?.removeFromParent();
303
+ this.raycastLine?.removeFromParent();
304
+ this._raycastHitPoint?.removeFromParent();
305
+ // console.log("Disable", this._connnectedCallback, this._disconnectedCallback);
306
+ // this.controllerGrip.removeEventListener("connected", this._connnectedCallback);
307
+ // this.controllerGrip.removeEventListener("disconnected", this._disconnectedCallback);
308
+
309
+ if (this.webXR) {
310
+ const i = this.webXR.Controllers.indexOf(this);
311
+ if (i >= 0)
312
+ this.webXR.Controllers.splice(i, 1);
313
+ }
314
+ }
315
+
316
+ // onDestroy(): void {
317
+ // console.log("destroyed", this.index);
318
+ // }
319
+
320
+ private _isConnected: boolean = false;
321
+
322
+ private onSourceConnected(e: { data: XRInputSource, target: any }) {
323
+ if (this._isConnected) {
324
+ console.warn("Received connected event for controller that is already connected", this.index, e);
325
+ return;
326
+ }
327
+ this._isConnected = true;
328
+ this.input = e.data;
329
+
330
+ if (this.type === ControllerType.Touch) {
331
+ this.onSelectStart();
332
+ }
333
+ }
334
+
335
+ private onSourceDisconnected(_e: any) {
336
+ if (!this._isConnected) {
337
+ console.warn("Received discnnected event for controller that is not connected", _e);
338
+ return;
339
+ }
340
+ this._isConnected = false;
341
+ if (this.type === ControllerType.Touch) {
342
+ this.onSelectEnd();
343
+ }
344
+ this.input = null;
345
+ }
346
+
347
+ rayRotation: Quaternion = new Quaternion();
348
+
349
+ private raycastUpdate(raycastLine: Line, wp: Vector3) {
350
+ const allowRaycastLineVisible = this.showRaycastLine && this.type !== ControllerType.Touch;
351
+ if (this.type === ControllerType.Touch) {
352
+ raycastLine.visible = false;
353
+ }
354
+ else if (this.isUsingHands) {
355
+ raycastLine.visible = !this.grabbed && allowRaycastLineVisible;
356
+ setWorldPosition(raycastLine, wp);
357
+ const jnts = this.hand!['joints'];
358
+ if (jnts) {
359
+ const wrist = jnts['wrist'];
360
+ if (wrist && this.grabbed && this.grabbed.isCloseGrab) {
361
+ const wr = this.getWristQuaternion();
362
+ if (wr)
363
+ this.rayRotation.copy(wr);
364
+ // this.rayRotation.slerp(wr, this.useSmoothing ? t * 2 : 1);
365
+ }
366
+ }
367
+ setWorldQuaternion(raycastLine, this.rayRotation);
368
+ }
369
+ else {
370
+ raycastLine.visible = allowRaycastLineVisible;
371
+ setWorldQuaternion(raycastLine, this.rayRotation);
372
+ setWorldPosition(raycastLine, wp);
373
+ }
374
+ }
375
+
376
+ update(): void {
377
+ if (!this.webXR) return;
378
+
379
+ // TODO: we should wait until we actually have models, this is just a workaround
380
+ if (this.context.time.frameCount % 60 === 0) {
381
+ this.setControllerLayers(this.controller, 2);
382
+ this.setControllerLayers(this.controllerGrip, 2);
383
+ this.setControllerLayers(this.hand, 2);
384
+ }
385
+
386
+ const subs = WebXRController.eventSubs[ControllerEvents.Update];
387
+ if (subs && subs.length > 0) {
388
+ for (const sub of subs) {
389
+ sub(this);
390
+ }
391
+ }
392
+
393
+ let t = 1;
394
+ if (this.type === ControllerType.PhysicalDevice) t = this.context.time.deltaTime / .1;
395
+ else if (this.isUsingHands && this.handPointerModel.pinched) t = this.context.time.deltaTime / .3;
396
+ this.rayRotation.slerp(getWorldQuaternion(this.controller), this.useSmoothing ? t : 1.0);
397
+ const wp = getWorldPosition(this.controller);
398
+
399
+ // hide hand pointer model, it's giant and doesn't really help
400
+ if (this.isUsingHands && this.handPointerModel.cursorObject) {
401
+ this.handPointerModel.cursorObject.visible = false;
402
+ }
403
+
404
+ if (this.raycastLine) {
405
+ this.raycastUpdate(this.raycastLine, wp);
406
+ }
407
+
408
+ this.lastHit = this.updateLastHit();
409
+
410
+ if (this.grabbed) {
411
+ this.grabbed.update();
412
+ }
413
+
414
+ this._selectionPressedLastFrame = this._selectionPressed;
415
+
416
+ if (this.selectStartCallback) {
417
+ this.selectStartCallback();
418
+ }
419
+ }
420
+
421
+ onUpdate(session: XRSession) {
422
+ this.lastHit = null;
423
+
424
+ if (!session || session.inputSources.length <= this.index) {
425
+ this.input = null;
426
+ return;
427
+ }
428
+ if (this.type === ControllerType.PhysicalDevice)
429
+ this.input = session.inputSources[this.index];
430
+ if (!this.input) return;
431
+ const rig = this.webXR!.Rig;
432
+ if (!rig) return;
433
+
434
+ if (this._didNotEndSelection && !this.handPointerModel.pinched) {
435
+ this._didNotEndSelection = false;
436
+ this.onSelectEnd();
437
+ }
438
+
439
+ this.updateStick(this.input);
440
+
441
+ const buttons = this.input?.gamepad?.buttons;
442
+
443
+ switch (this.input.handedness) {
444
+ case "left":
445
+ this.movementUpdate(rig, buttons);
446
+ break;
447
+
448
+ case "right":
449
+ this.rotationUpdate(rig, buttons);
450
+ break;
451
+ }
452
+ }
453
+
454
+
455
+ private movementUpdate(rig: Object3D, buttons?: readonly GamepadButton[]) {
456
+ const speedFactor = 3 * WebXRController.MovementSpeedFactor;
457
+ const powFactor = 2;
458
+ const speed = Mathf.clamp01(this.joystick.length() * 2);
459
+
460
+ const sideDir = this.joystick.x > 0 ? 1 : -1;
461
+ let side = Math.pow(this.joystick.x, powFactor);
462
+ side *= sideDir;
463
+ side *= speed;
464
+
465
+
466
+ const forwardDir = this.joystick.y > 0 ? 1 : -1;
467
+ let forward = Math.pow(this.joystick.y, powFactor);
468
+ forward *= forwardDir;
469
+ side *= speed;
470
+
471
+ rig.getWorldQuaternion(this.worldRot);
472
+ this.movementVector.set(side, 0, forward);
473
+ this.movementVector.applyQuaternion(this.webXR!.TransformOrientation);
474
+ this.movementVector.y = 0;
475
+ this.movementVector.applyQuaternion(this.worldRot);
476
+ this.movementVector.multiplyScalar(speedFactor * this.context.time.deltaTime);
477
+ rig.position.add(this.movementVector);
478
+
479
+ if (this.isUsingHands)
480
+ this.runTeleport(rig, buttons);
481
+ }
482
+
483
+ private rotationUpdate(rig: Object3D, buttons?: readonly GamepadButton[]) {
484
+ const rotate = this.joystick.x;
485
+ const rotAbs = Math.abs(rotate);
486
+ if (rotAbs < 0.4) {
487
+ this.didRotate = false;
488
+ }
489
+ else if (rotAbs > .5 && !this.didRotate) {
490
+ const dir = rotate > 0 ? -1 : 1;
491
+ rig.rotateY(Mathf.toRadians(30 * dir));
492
+ this.didRotate = true;
493
+ }
494
+
495
+ this.runTeleport(rig, buttons);
496
+ }
497
+ private _pinchStartTime: number | undefined = undefined;
498
+
499
+ private runTeleport(rig: Object3D, buttons?: readonly GamepadButton[]) {
500
+ let teleport = -this.joystick.y;
501
+ if (this.hand?.visible && !this.grabbed) {
502
+ const pinched = this.handPointerModel.isPinched();
503
+ if (pinched && this._pinchStartTime === undefined) {
504
+ this._pinchStartTime = this.context.time.time;
505
+ }
506
+ if (pinched && this._pinchStartTime && this.context.time.time - this._pinchStartTime > .8) {
507
+ // hacky approach for basic hand teleportation -
508
+ // we teleport if we pinch and the back of the hand points down (open hand gesture)
509
+ // const v1 = new Vector3();
510
+ // const worldQuaternion = new Quaternion();
511
+ // this.controller.getWorldQuaternion(worldQuaternion);
512
+ // v1.copy(this.controller.up).applyQuaternion(worldQuaternion);
513
+ // const dotPr = -v1.dot(this.controller.up);
514
+ teleport = this.handPointerModel.isPinched() ? 1 : 0;
515
+ }
516
+ if (!pinched) this._pinchStartTime = undefined;
517
+ }
518
+ else this._pinchStartTime = undefined;
519
+
520
+ const inVR = this.webXR!.IsInVR;
521
+ const xrRig = this.webXR!.Rig;
522
+ let doTeleport = teleport > .5 && inVR;
523
+ let isInMiniatureMode = xrRig ? xrRig?.scale?.x < .999 : false;
524
+ let newRigScale: number | null = null;
525
+
526
+ if (buttons && this.input && !this.input.hand) {
527
+ for (let i = 0; i < buttons.length; i++) {
528
+ const btn = buttons[i];
529
+ // button[4] seems to be the A button if it exists. On hololens it's randomly pressed though for hands
530
+ // see https://www.w3.org/TR/webxr-gamepads-module-1/#xr-standard-gamepad-mapping
531
+ if (i === 4) {
532
+ if (btn.pressed && !this.didChangeScale && inVR) {
533
+ this.didChangeScale = true;
534
+ const rig = xrRig;
535
+ if (rig) {
536
+ const args = this.switchScale(rig, doTeleport, isInMiniatureMode, newRigScale);
537
+ doTeleport = args.doTeleport;
538
+ isInMiniatureMode = args.isInMiniatureMode;
539
+ newRigScale = args.newRigScale;
540
+ }
541
+ }
542
+ else if (!btn.pressed)
543
+ this.didChangeScale = false;
544
+ }
545
+ }
546
+ }
547
+
548
+ if (doTeleport) {
549
+ if (!this.didTeleport) {
550
+ const rc = this.raycast();
551
+ this.didTeleport = true;
552
+ if (rc && rc.length > 0) {
553
+ const hit = rc[0];
554
+ if (isInMiniatureMode || this.isValidTeleportTarget(hit.object)) {
555
+ const point = hit.point;
556
+ setWorldPosition(rig, point);
557
+ }
558
+ }
559
+ }
560
+ }
561
+ else if (teleport < .1) {
562
+ this.didTeleport = false;
563
+ }
564
+
565
+ if (newRigScale !== null) {
566
+ rig.scale.set(newRigScale, newRigScale, newRigScale);
567
+ rig.updateMatrixWorld();
568
+ }
569
+ }
570
+
571
+
572
+ private isValidTeleportTarget(obj: Object3D): boolean {
573
+ return GameObject.getComponentInParent(obj, TeleportTarget) != null;
574
+ }
575
+
576
+ private switchScale(rig: Object3D, doTeleport: boolean, isInMiniatureMode: boolean, newRigScale: number | null) {
577
+ if (!isInMiniatureMode) {
578
+ isInMiniatureMode = true;
579
+ doTeleport = true;
580
+ newRigScale = .1;
581
+ WebXRController.MovementSpeedFactor = newRigScale * 2;
582
+ const cam = this.context.mainCamera as PerspectiveCamera;
583
+ WebXRController.PreviousCameraFarDistance = cam.far;
584
+ cam.far /= newRigScale;
585
+ }
586
+ else {
587
+ isInMiniatureMode = false;
588
+ rig.scale.set(1, 1, 1);
589
+ newRigScale = 1;
590
+ WebXRController.MovementSpeedFactor = 1;
591
+ const cam = this.context.mainCamera as PerspectiveCamera;
592
+ if (WebXRController.PreviousCameraFarDistance)
593
+ cam.far = WebXRController.PreviousCameraFarDistance;
594
+ }
595
+ return { doTeleport, isInMiniatureMode, newRigScale }
596
+ }
597
+
598
+ private updateStick(inputSource: XRInputSource) {
599
+ if (!inputSource || !inputSource.gamepad || inputSource.gamepad.axes?.length < 4) return;
600
+ this.joystick.x = inputSource.gamepad.axes[2];
601
+ this.joystick.y = inputSource.gamepad.axes[3];
602
+ }
603
+
604
+ private updateLastHit(): Intersection | null {
605
+ const rc = this.raycast();
606
+ const hit = rc ? rc[0] : null;
607
+ this.lastHit = hit;
608
+ let factor = 1;
609
+ if (this.webXR!.Rig) {
610
+ factor /= this.webXR!.Rig.scale.x;
611
+ }
612
+ // if (!hit) factor = 0;
613
+
614
+ if (this.raycastLine) {
615
+ this.raycastLine.scale.z = factor * (this.lastHit?.distance ?? 9999);
616
+ const mat = this.raycastLine.material as LineBasicMaterial;
617
+ if (hit != null) mat.color = WebXRController.raycastColor;
618
+ else mat.color = WebXRController.raycastNoHitColor;
619
+ }
620
+ if (this._raycastHitPoint) {
621
+ if (this.lastHit != null) {
622
+ this._raycastHitPoint.position.z = -1;// -this.lastHit.distance;
623
+ const scale = Mathf.clamp(this.lastHit.distance * .01 * factor, .015, .1);
624
+ this._raycastHitPoint.scale.set(scale, scale, scale);
625
+ }
626
+ this._raycastHitPoint.visible = this.lastHit !== null && this.lastHit !== undefined;
627
+ }
628
+ return hit;
629
+ }
630
+
631
+ private onSelectStart() {
632
+ if (!this.context.connection.allowEditing) return;
633
+ // console.log("SELECT START", _event);
634
+ // if we process the event immediately the controller
635
+ // world positions are not yet correctly updated and we have info from the last frame
636
+ // so we delay the event processing one frame
637
+ // only necessary for AR - ideally we can get it to work right here
638
+ // but should be fine as a workaround for now
639
+ this.selectStartCallback = () => this.onHandleSelectStart();
640
+ }
641
+
642
+ private selectStartCallback: Function | null = null;
643
+ private lastSelectStartObject: Object3D | null = null;;
644
+
645
+ private onHandleSelectStart() {
646
+ this.selectStartCallback = null;
647
+ this._selectionPressed = true;
648
+ this._selectionStartTime = this.context.time.time;
649
+ this._selectionEndTime = 1000;
650
+ // console.log("DOWN", this.index, WebXRController.eventSubs);
651
+
652
+ // let maxDistance = this.isUsingHands ? .1 : undefined;
653
+ let intersections: Intersection[] | null = null;
654
+ let closeGrab: boolean = false;
655
+ if (this.isUsingHands) {
656
+ intersections = this.overlap();
657
+ if (intersections.length <= 0) {
658
+ intersections = this.raycast();
659
+ closeGrab = false;
660
+ }
661
+ else {
662
+ closeGrab = true;
663
+ }
664
+ }
665
+ else intersections = this.raycast();
666
+
667
+ if (debug)
668
+ console.log("onHandleSelectStart", "close grab? " + closeGrab, "intersections", intersections);
669
+
670
+ if (intersections && intersections.length > 0) {
671
+ for (const intersection of intersections) {
672
+ const object = intersection.object;
673
+ this.lastSelectStartObject = object;
674
+ const args = { selected: object, grab: object };
675
+ const subs = WebXRController.eventSubs[ControllerEvents.SelectStart];
676
+ if (subs && subs.length > 0) {
677
+ for (const sub of subs) {
678
+ sub(this, args);
679
+ }
680
+ }
681
+ if (args.grab !== object && debug)
682
+ console.log("Grabbed object changed", "original", object, "new", args.grab);
683
+ if (args.grab) {
684
+ this.grabbed = AttachedObject.TryTake(this, args.grab, intersection, closeGrab);
685
+ }
686
+ break;
687
+ }
688
+ }
689
+ else {
690
+ const subs = WebXRController.eventSubs[ControllerEvents.SelectStart];
691
+ const args = { selected: null, grab: null };
692
+ if (subs && subs.length > 0) {
693
+ for (const sub of subs) {
694
+ sub(this, args);
695
+ }
696
+ }
697
+ }
698
+ }
699
+
700
+ private _didNotEndSelection: boolean = false;
701
+
702
+ private onSelectEnd() {
703
+ if (this.isUsingHands) {
704
+ if (this.handPointerModel.pinched) {
705
+ this._didNotEndSelection = true;
706
+ return;
707
+ }
708
+ }
709
+
710
+ if (!this._selectionPressed) return;
711
+ this.selectStartCallback = null;
712
+ this._selectionPressed = false;
713
+ this._selectionEndTime = this.context.time.time;
714
+
715
+ const args = { grab: this.grabbed?.selected ?? this.lastSelectStartObject };
716
+ const subs = WebXRController.eventSubs[ControllerEvents.SelectEnd];
717
+ if (subs && subs.length > 0) {
718
+ for (const sub of subs) {
719
+ sub(this, args);
720
+ }
721
+ }
722
+
723
+ if (this.grabbed) {
724
+ this.grabbed.free();
725
+ this.grabbed = null;
726
+ }
727
+ }
728
+
729
+ private testIsVisible(obj: Object3D | null): boolean {
730
+ if (!obj) return false;
731
+ if (GameObject.isActiveInHierarchy(obj) === false) return false;
732
+ if (UIRaycastUtils.isInteractable(obj) === false) {
733
+ return false;
734
+ }
735
+ return true;
736
+ // if (!obj.visible) return false;
737
+ // return this.testIsVisible(obj.parent);
738
+ }
739
+
740
+ private setControllerLayers(obj: Object3D, layer: number) {
741
+ if (!obj) return;
742
+ obj.layers.set(layer);
743
+ if (obj.children) {
744
+ for (const ch of obj.children) {
745
+ if (this.grabbed?.selected === ch || this.grabbed?.selectedMesh === ch) {
746
+ continue;
747
+ }
748
+ this.setControllerLayers(ch, layer);
749
+ }
750
+ }
751
+ }
752
+
753
+ public getRay(): Ray {
754
+ const ray = new Ray();
755
+ // this.tempMatrix.identity().extractRotation(this.controller.matrixWorld);
756
+ // ray.origin.setFromMatrixPosition(this.controller.matrixWorld);
757
+ ray.origin.copy(getWorldPosition(this.controller));
758
+ ray.direction.set(0, 0, -1).applyQuaternion(this.rayRotation);
759
+ return ray;
760
+ }
761
+
762
+ private closeGrabBoundingBoxHelper?: BoxHelper;
763
+
764
+ public overlap(): Intersection[] {
765
+ const overlapCenter = (this.isUsingHands && this.handPointerModel) ? this.handPointerModel.pointerObject : this.controllerGrip;
766
+
767
+ if (debug) {
768
+ if (!this.closeGrabBoundingBoxHelper && overlapCenter) {
769
+ this.closeGrabBoundingBoxHelper = new BoxHelper(overlapCenter, 0xffff00);
770
+ this.scene.add(this.closeGrabBoundingBoxHelper);
771
+ }
772
+
773
+ if (this.closeGrabBoundingBoxHelper && overlapCenter) {
774
+ this.closeGrabBoundingBoxHelper.setFromObject(overlapCenter);
775
+ }
776
+ }
777
+
778
+ if (!overlapCenter)
779
+ return new Array<Intersection>();
780
+
781
+ const wp = getWorldPosition(overlapCenter).clone();
782
+ return this.context.physics.sphereOverlap(wp, .02);
783
+ }
784
+
785
+ public raycast(): Intersection[] {
786
+ const opts = new RaycastOptions();
787
+ opts.layerMask = new Layers();
788
+ opts.layerMask.enableAll();
789
+ opts.layerMask.disable(2);
790
+ opts.ray = this.getRay();
791
+ const hits = this.context.physics.raycast(opts);
792
+ for (let i = 0; i < hits.length; i++) {
793
+ const hit = hits[i];
794
+ const obj = hit.object;
795
+ if (!this.testIsVisible(obj)) {
796
+ hits.splice(i, 1);
797
+ i--;
798
+ continue;
799
+ }
800
+ hit.object = UIRaycastUtils.getObject(obj);
801
+ break;
802
+ }
803
+ // console.log(...hits);
804
+ return hits;
805
+ }
806
+ }
807
+
808
+
809
+ export enum AttachedObjectEvents {
810
+ WillTake = "WillTake",
811
+ DidTake = "DidTake",
812
+ WillFree = "WillFree",
813
+ DidFree = "DidFree",
814
+ }
815
+
816
+ export class AttachedObject {
817
+
818
+ public static Events: { [key: string]: Function[] } = {};
819
+ public static AddEventListener(event: AttachedObjectEvents, callback: Function): Function {
820
+ if (!AttachedObject.Events[event]) AttachedObject.Events[event] = [];
821
+ AttachedObject.Events[event].push(callback);
822
+ return callback;
823
+ }
824
+ public static RemoveEventListener(event: AttachedObjectEvents, callback: Function | null) {
825
+ if (!callback) return;
826
+ if (!AttachedObject.Events[event]) return;
827
+ const idx = AttachedObject.Events[event].indexOf(callback);
828
+ if (idx >= 0) AttachedObject.Events[event].splice(idx, 1);
829
+ }
830
+
831
+
832
+ public static Current: AttachedObject[] = [];
833
+
834
+ private static Register(obj: AttachedObject) {
835
+
836
+ if (!this.Current.find(x => x === obj)) {
837
+ this.Current.push(obj);
838
+ }
839
+ }
840
+
841
+ private static Remove(obj: AttachedObject) {
842
+ const i = this.Current.indexOf(obj);
843
+ if (i >= 0) {
844
+ this.Current.splice(i, 1);
845
+ }
846
+ }
847
+
848
+ public static TryTake(controller: WebXRController, candidate: Object3D, intersection: Intersection, closeGrab: boolean): AttachedObject | null {
849
+ const interactable = GameObject.getComponentInParent(candidate, Interactable);
850
+ if (!interactable) {
851
+ if (debug)
852
+ console.warn("Prevented taking object that is not interactable", candidate);
853
+ return null;
854
+ }
855
+ else candidate = interactable.gameObject;
856
+
857
+
858
+ let objectToAttach = candidate;
859
+ const sync = GameObject.getComponentInParent(candidate, SyncedTransform);
860
+ if (sync) {
861
+ sync.requestOwnership();
862
+ objectToAttach = sync.gameObject;
863
+ }
864
+
865
+ for (const o of this.Current) {
866
+ if (o.selected === objectToAttach) {
867
+ if (o.controller === controller) return o;
868
+ o.free();
869
+ o.Take(controller, objectToAttach, candidate, sync, interactable, intersection, closeGrab);
870
+ return o;
871
+ }
872
+ }
873
+
874
+ const att = new AttachedObject();
875
+ att.Take(controller, objectToAttach, candidate, sync, interactable, intersection, closeGrab);
876
+ return att;
877
+ }
878
+
879
+
880
+ public sync: SyncedTransform | null = null;
881
+ public selected: Object3D | null = null;
882
+ public selectedParent: Object3D | null = null;
883
+ public selectedMesh: Mesh | null = null;
884
+ public controller: WebXRController | null = null;
885
+ public grabTime: number = 0;
886
+ public grabUUID: string = "";
887
+ public isCloseGrab: boolean = false; // when taken via sphere cast with hands
888
+
889
+ private originalMaterial: Material | Material[] | null = null;
890
+ private usageMarker: UsageMarker | null = null;
891
+ private rigidbodies: Rigidbody[] | null = null;
892
+ private didReparent: boolean = false;
893
+ private grabDistance: number = 0;
894
+ private interactable: Interactable | null = null;
895
+ private positionSource: Object3D | null = null;
896
+
897
+ private Take(controller: WebXRController, take: Object3D, hit: Object3D, sync: SyncedTransform | null, _interactable: Interactable,
898
+ intersection: Intersection, closeGrab: boolean)
899
+ : AttachedObject {
900
+ console.assert(take !== null, "Expected object to be taken but was", take);
901
+
902
+ if (controller.isUsingHands) {
903
+ this.positionSource = closeGrab ? controller.wrist : controller.controller;
904
+ }
905
+ else {
906
+ this.positionSource = controller.controller;
907
+ }
908
+ if (!this.positionSource) {
909
+ console.warn("No position source");
910
+ return this;
911
+ }
912
+
913
+ const args = { controller, take, hit, sync, interactable: _interactable };
914
+ AttachedObject.Events.WillTake?.forEach(x => x(this, args));
915
+
916
+
917
+ const mesh = hit as Mesh;
918
+ if (mesh?.material) {
919
+ this.originalMaterial = mesh.material;
920
+ if (!Array.isArray(mesh.material)) {
921
+ mesh.material = (mesh.material as Material).clone();
922
+ if (mesh.material && mesh.material["emissive"])
923
+ mesh.material["emissive"].b = .2;
924
+ }
925
+ }
926
+
927
+ this.selected = take;
928
+ if (!this.selectedParent) {
929
+ this.selectedParent = take.parent;
930
+ }
931
+ this.selectedMesh = mesh;
932
+ this.controller = controller;
933
+ this.interactable = _interactable;
934
+ this.isCloseGrab = closeGrab;
935
+ // if (interactable.canGrab) {
936
+ // this.didReparent = true;
937
+ // this.device.controller.attach(take);
938
+ // }
939
+ // else
940
+ this.didReparent = false;
941
+
942
+
943
+ this.sync = sync;
944
+ this.grabTime = controller.context.time.time;
945
+ this.grabUUID = Date.now().toString();
946
+ this.usageMarker = GameObject.addNewComponent(this.selected, UsageMarker);
947
+ this.rigidbodies = GameObject.getComponentsInChildren(this.selected, Rigidbody);
948
+ getWorldPosition(this.positionSource, this.lastControllerWorldPos);
949
+ const getGrabPoint = () => closeGrab ? this.lastControllerWorldPos.clone() : intersection.point.clone();
950
+ this.grabDistance = getGrabPoint().distanceTo(this.lastControllerWorldPos);
951
+ this.totalChangeAlongDirection = 0.0;
952
+
953
+ // we're storing position relative to the grab point
954
+ // we're storing rotation relative to the ray
955
+ this.localPositionOffsetToGrab = this.selected.worldToLocal(getGrabPoint());
956
+ const rot = controller.isUsingHands && closeGrab ? this.controller.getWristQuaternion()!.clone() : controller.rayRotation.clone();
957
+ getWorldQuaternion(this.selected, this.localQuaternionToGrab).premultiply(rot.invert());
958
+
959
+ const rig = this.controller.webXR!.Rig;
960
+ if (rig)
961
+ this.rigPositionLastFrame.copy(getWorldPosition(rig))
962
+
963
+ Avatar_POI.Add(controller.context, this.selected);
964
+ AttachedObject.Register(this);
965
+
966
+ if (this.sync) {
967
+ this.sync.fastMode = true;
968
+ }
969
+
970
+ AttachedObject.Events.DidTake?.forEach(x => x(this, args));
971
+
972
+ return this;
973
+ }
974
+
975
+ public free(): void {
976
+ if (!this.selected) return;
977
+
978
+ const args = { controller: this.controller, take: this.selected, hit: this.selected, sync: this.sync, interactable: null };
979
+ AttachedObject.Events.WillFree?.forEach(x => x(this, args));
980
+
981
+ Avatar_POI.Remove(this.controller!.context, this.selected);
982
+ AttachedObject.Remove(this);
983
+
984
+ if (this.sync) {
985
+ this.sync.fastMode = false;
986
+ }
987
+
988
+ const mesh = this.selectedMesh;
989
+ if (mesh && this.originalMaterial && mesh.material) {
990
+ mesh.material = this.originalMaterial;
991
+ }
992
+
993
+ const object = this.selected;
994
+ // only attach the object back if it has a parent
995
+ // no parent means it was destroyed while holding it!
996
+ if (this.didReparent && object.parent) {
997
+ const prevParent = this.selectedParent;
998
+ if (prevParent) prevParent.attach(object);
999
+ else this.controller?.context.scene.attach(object);
1000
+ }
1001
+
1002
+ this.usageMarker?.destroy();
1003
+
1004
+ if (this.controller)
1005
+ this.controller.grabbed = null;
1006
+ this.selected = null;
1007
+ this.selectedParent = null;
1008
+ this.selectedMesh = null;
1009
+ this.sync = null;
1010
+
1011
+
1012
+ // TODO: make throwing work again
1013
+ if (this.rigidbodies) {
1014
+ for (const rb of this.rigidbodies) {
1015
+ rb.wakeUp();
1016
+ rb.setVelocity(rb.smoothedVelocity);
1017
+ }
1018
+ }
1019
+ this.rigidbodies = null;
1020
+
1021
+ this.localPositionOffsetToGrab = null;
1022
+ this.quaternionLerp = null;
1023
+
1024
+ AttachedObject.Events.DidFree?.forEach(x => x(this, args));
1025
+ }
1026
+
1027
+ public grabPoint: Vector3 = new Vector3();
1028
+
1029
+ private localPositionOffsetToGrab: Vector3 | null = null;
1030
+ private localPositionOffsetToGrab_worldSpace: Vector3 = new Vector3();
1031
+ private localQuaternionToGrab: Quaternion = new Quaternion(0, 0, 0, 1);
1032
+ private targetDir: Vector3 | null = null;
1033
+ private quaternionLerp: Quaternion | null = null;
1034
+
1035
+ private controllerDir = new Vector3();
1036
+ private controllerWorldPos = new Vector3();
1037
+ private lastControllerWorldPos = new Vector3();
1038
+ private controllerPosDelta = new Vector3();
1039
+ private totalChangeAlongDirection = 0.0;
1040
+ private rigPositionLastFrame = new Vector3();
1041
+
1042
+ private controllerMovementSinceLastFrame() {
1043
+ if (!this.positionSource || !this.controller) return 0.0;
1044
+
1045
+ // controller direction
1046
+ this.controllerDir.set(0, 0, -1);
1047
+ this.controllerDir.applyQuaternion(this.controller.rayRotation);
1048
+
1049
+ // controller delta
1050
+ getWorldPosition(this.positionSource, this.controllerWorldPos);
1051
+ this.controllerPosDelta.copy(this.controllerWorldPos);
1052
+ this.controllerPosDelta.sub(this.lastControllerWorldPos);
1053
+ this.lastControllerWorldPos.copy(this.controllerWorldPos);
1054
+ const rig = this.controller.webXR!.Rig;
1055
+ if (rig) {
1056
+ const rigPos = getWorldPosition(rig);
1057
+ const rigDelta = this.rigPositionLastFrame.sub(rigPos);
1058
+ this.controllerPosDelta.add(rigDelta);
1059
+ this.rigPositionLastFrame.copy(rigPos);
1060
+ }
1061
+
1062
+ // calculate delta along direction
1063
+ const changeAlongControllerDirection = this.controllerDir.dot(this.controllerPosDelta);
1064
+
1065
+ return changeAlongControllerDirection;
1066
+ }
1067
+
1068
+ public update() {
1069
+ if (this.rigidbodies)
1070
+ for (const rb of this.rigidbodies)
1071
+ rb.resetVelocities();
1072
+ // TODO: add/use sync lost ownership event
1073
+ if (this.sync && this.controller && this.controller.context.connection.isInRoom) {
1074
+ const td = this.controller.context.time.time - this.grabTime;
1075
+ // if (time.frameCount % 60 === 0) {
1076
+ // console.log("ownership?", this.selected.name, this.sync.hasOwnership(), td)
1077
+ // }
1078
+ if (td > 3) {
1079
+ // if (time.frameCount % 60 === 0) {
1080
+ // console.log(this.sync.hasOwnership())
1081
+ // }
1082
+ if (this.sync.hasOwnership() === false) {
1083
+ console.log("no ownership, will leave", this.sync.guid);
1084
+ this.free();
1085
+ }
1086
+ }
1087
+ }
1088
+ if (this.interactable && !this.interactable.canGrab) return;
1089
+
1090
+ if (!this.didReparent && this.selected && this.controller) {
1091
+
1092
+ const rigScale = this.controller.webXR!.Rig?.scale.x ?? 1.0;
1093
+
1094
+ this.totalChangeAlongDirection += this.controllerMovementSinceLastFrame();
1095
+ // console.log(this.totalChangeAlongDirection);
1096
+
1097
+ // alert("yo: " + this.controller.webXR.Rig?.scale.x); // this is 0.1 on Hololens
1098
+ let currentDist = 1.0;
1099
+ if (this.controller.type === ControllerType.PhysicalDevice) // only for controllers and not on touch (AR touches are controllers)
1100
+ {
1101
+ currentDist = Math.max(0.0, 1 + this.totalChangeAlongDirection * 2.0 / rigScale);
1102
+ currentDist = currentDist * currentDist * currentDist;
1103
+ }
1104
+ if (this.grabDistance / rigScale < 0.8) currentDist = 1.0; // don't accelerate if this is a close grab, want full control
1105
+
1106
+ if (!this.targetDir) {
1107
+ this.targetDir = new Vector3();
1108
+ }
1109
+ this.targetDir.set(0, 0, -this.grabDistance * currentDist);
1110
+ const target = this.targetDir.applyQuaternion(this.controller.rayRotation).add(this.controllerWorldPos);
1111
+
1112
+ // apply rotation
1113
+ const targetQuat = this.controller.rayRotation.clone().multiplyQuaternions(this.controller.rayRotation, this.localQuaternionToGrab);
1114
+ if (!this.quaternionLerp) {
1115
+ this.quaternionLerp = targetQuat.clone();
1116
+ }
1117
+ this.quaternionLerp.slerp(targetQuat, this.controller.useSmoothing ? this.controller.context.time.deltaTime / .03 : 1.0);
1118
+ setWorldQuaternion(this.selected, this.quaternionLerp);
1119
+ this.selected.updateWorldMatrix(false, false); // necessary so that rotation is correct for the following position update
1120
+
1121
+ // apply position
1122
+ this.grabPoint.copy(target);
1123
+ // apply local grab offset
1124
+ if (this.localPositionOffsetToGrab) {
1125
+ this.localPositionOffsetToGrab_worldSpace.copy(this.localPositionOffsetToGrab);
1126
+ this.selected.localToWorld(this.localPositionOffsetToGrab_worldSpace).sub(getWorldPosition(this.selected));
1127
+ target.sub(this.localPositionOffsetToGrab_worldSpace);
1128
+ }
1129
+ setWorldPosition(this.selected, target);
1130
+ }
1131
+
1132
+
1133
+ if (this.rigidbodies != null) {
1134
+ for (const rb of this.rigidbodies) {
1135
+ rb.wakeUp();
1136
+ }
1137
+ }
1138
+
1139
+ InstancingUtil.markDirty(this.selected, true);
1140
+ }
1141
+ }