-
Notifications
You must be signed in to change notification settings - Fork 22
Expand file tree
/
Copy pathxrInitializer.ts
More file actions
386 lines (320 loc) · 13.1 KB
/
Copy pathxrInitializer.ts
File metadata and controls
386 lines (320 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
import * as THREE from 'three';
import { createLogger, createErrorMetadata } from '@/utils/logger';
import { debugState } from '@/utils/debugState';
import { XRSessionManager, XRControllerEvent } from './xrSessionManager';
import { XRSettings } from '../types/xr';
import { SceneManager } from '../../visualisation/managers/sceneManager';
const logger = createLogger('XRInitializer');
export class XRInitializer {
private static instance: XRInitializer;
private xrSessionManager: XRSessionManager;
private sceneManager: SceneManager;
private scene: THREE.Scene;
private camera: THREE.PerspectiveCamera;
private teleportMarker: THREE.Mesh | null = null;
private floorPlane: THREE.Mesh | null = null;
private settings: XRSettings | null = null;
private raycaster: THREE.Raycaster = new THREE.Raycaster();
private controllerIntersections: Map<THREE.XRTargetRaySpace, THREE.Intersection[]> = new Map();
// Teleportation state
private isTeleporting: boolean = false;
private teleportPosition: THREE.Vector3 = new THREE.Vector3();
// Movement state
private movementEnabled: boolean = true;
private movementSpeed: number = 1.0;
// Controller handlers
private controllerSelectStartUnsubscribe: (() => void) | null = null;
private controllerSelectEndUnsubscribe: (() => void) | null = null;
private controllerSqueezeStartUnsubscribe: (() => void) | null = null;
private controllerSqueezeEndUnsubscribe: (() => void) | null = null;
private constructor(xrSessionManager: XRSessionManager) {
this.xrSessionManager = xrSessionManager;
this.sceneManager = SceneManager.getInstance();
this.scene = this.sceneManager.getScene();
// Get camera and ensure it's a PerspectiveCamera
const camera = this.sceneManager.getCamera();
if (!camera || !(camera instanceof THREE.PerspectiveCamera)) {
logger.warn('PerspectiveCamera not available from SceneManager, creating default camera');
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.camera.position.z = 5;
} else {
// We have a valid PerspectiveCamera
this.camera = camera as THREE.PerspectiveCamera;
}
// Setup XR interactions
this.setupXRInteractions();
}
public static getInstance(xrSessionManager: XRSessionManager): XRInitializer {
if (!XRInitializer.instance) {
XRInitializer.instance = new XRInitializer(xrSessionManager);
}
return XRInitializer.instance;
}
// Initialize XR capabilities with current settings
public initialize(settings: XRSettings): void {
this.settings = settings;
// The XRSettings interface was updated, so we access properties directly.
// We also need to check if settings itself is null before accessing its properties.
if (settings) {
this.movementEnabled = true; // Assuming movement is enabled if XR settings are present
this.movementSpeed = settings.movementSpeed || 1.0;
// Setup floor if enabled
if (settings.showFloor) {
this.createFloor();
} else if (this.floorPlane) {
this.scene.remove(this.floorPlane);
this.floorPlane.geometry.dispose();
(this.floorPlane.material as THREE.Material).dispose();
this.floorPlane = null;
}
// Setup teleport marker if teleport is enabled
if (settings.teleportEnabled) {
this.createTeleportMarker();
} else if (this.teleportMarker) {
this.scene.remove(this.teleportMarker);
this.teleportMarker.geometry.dispose();
(this.teleportMarker.material as THREE.Material).dispose();
this.teleportMarker = null;
}
}
if (debugState.isEnabled()) {
logger.info('XR initializer initialized with settings');
}
}
// Setup all XR interactions
private setupXRInteractions(): void {
// Register for controller events
this.controllerSelectStartUnsubscribe = this.xrSessionManager.onSelectStart(
this.handleControllerSelectStart.bind(this)
);
this.controllerSelectEndUnsubscribe = this.xrSessionManager.onSelectEnd(
this.handleControllerSelectEnd.bind(this)
);
this.controllerSqueezeStartUnsubscribe = this.xrSessionManager.onSqueezeStart(
this.handleControllerSqueezeStart.bind(this)
);
this.controllerSqueezeEndUnsubscribe = this.xrSessionManager.onSqueezeEnd(
this.handleControllerSqueezeEnd.bind(this)
);
// Add render callback for continuous interaction checks
this.sceneManager.addRenderCallback(this.update.bind(this));
if (debugState.isEnabled()) {
logger.info('XR interactions setup complete');
}
}
// Create floor plane for reference and teleportation
private createFloor(): void {
if (this.floorPlane) {
return;
}
const geometry = new THREE.PlaneGeometry(20, 20);
const material = new THREE.MeshBasicMaterial({
color: 0x808080,
transparent: true,
opacity: 0.2,
side: THREE.DoubleSide
});
this.floorPlane = new THREE.Mesh(geometry, material);
this.floorPlane.rotation.x = -Math.PI / 2;
this.floorPlane.position.y = 0;
this.floorPlane.receiveShadow = true;
this.floorPlane.name = 'xr-floor';
this.scene.add(this.floorPlane);
if (debugState.isEnabled()) {
logger.info('XR floor plane created');
}
}
// Create teleport marker for showing valid teleport locations
private createTeleportMarker(): void {
if (this.teleportMarker) {
return;
}
const geometry = new THREE.RingGeometry(0.15, 0.2, 32);
const material = new THREE.MeshBasicMaterial({
color: 0x00ff00,
transparent: true,
opacity: 0.6,
side: THREE.DoubleSide
});
this.teleportMarker = new THREE.Mesh(geometry, material);
this.teleportMarker.rotation.x = -Math.PI / 2;
this.teleportMarker.visible = false;
this.teleportMarker.name = 'teleport-marker';
this.scene.add(this.teleportMarker);
if (debugState.isEnabled()) {
logger.info('Teleport marker created');
}
}
// Update loop for continuous interactions
private update(): void {
if (!this.xrSessionManager.isSessionActive()) {
return;
}
// Get controllers
const controllers = this.xrSessionManager.getControllers();
// Update controller interactions
controllers.forEach(controller => {
this.updateControllerInteractions(controller);
});
}
// Check for intersections with objects
private updateControllerInteractions(controller: THREE.XRTargetRaySpace): void {
// Skip if there's no session
if (!this.xrSessionManager.isSessionActive()) {
return;
}
// Initialize raycaster from controller
const tempMatrix = new THREE.Matrix4();
tempMatrix.identity().extractRotation(controller.matrixWorld);
this.raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
this.raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix);
// Store intersections for this controller
const intersections: THREE.Intersection[] = [];
// Check for floor intersection if teleport is enabled
if (this.floorPlane && this.settings?.teleportEnabled) {
const floorIntersects = this.raycaster.intersectObject(this.floorPlane);
if (floorIntersects.length > 0) {
intersections.push(...floorIntersects);
// Update teleport marker position if currently teleporting
if (this.isTeleporting && this.teleportMarker) {
this.teleportPosition.copy(floorIntersects[0].point);
this.teleportMarker.position.copy(this.teleportPosition);
this.teleportMarker.visible = true;
}
} else if (this.isTeleporting && this.teleportMarker) {
// Hide marker if not pointing at floor
this.teleportMarker.visible = false;
}
}
// Store intersections for this controller
this.controllerIntersections.set(controller, intersections);
}
// Handle controller select start event (trigger press)
private handleControllerSelectStart(event: XRControllerEvent): void {
const { controller } = event;
// Start teleportation if enabled
if (this.settings?.teleportEnabled) {
this.isTeleporting = true;
// Show teleport marker if there's a valid intersection
const intersections = this.controllerIntersections.get(controller) || [];
if (intersections.length > 0 && this.floorPlane && this.teleportMarker) {
const floorIntersect = intersections.find(
i => i.object === this.floorPlane
);
if (floorIntersect) {
this.teleportPosition.copy(floorIntersect.point);
this.teleportMarker.position.copy(this.teleportPosition);
this.teleportMarker.visible = true;
}
}
}
}
// Handle controller select end event (trigger release)
private handleControllerSelectEnd(event: XRControllerEvent): void {
// Complete teleportation if in progress
if (this.isTeleporting && this.teleportMarker && this.teleportMarker.visible) {
// Get camera position but keep y-height the same
const cameraPosition = new THREE.Vector3();
cameraPosition.setFromMatrixPosition(this.camera.matrixWorld);
// Calculate teleport offset (where we want camera to end up)
const offsetX = this.teleportPosition.x - cameraPosition.x;
const offsetZ = this.teleportPosition.z - cameraPosition.z;
// Find camera rig/offset parent - in WebXR the camera is often a child of a rig
let cameraRig = this.camera.parent;
if (cameraRig) {
// Apply offset to camera rig's position
cameraRig.position.x += offsetX;
cameraRig.position.z += offsetZ;
} else {
// Fallback to moving camera directly if no rig
this.camera.position.x += offsetX;
this.camera.position.z += offsetZ;
}
// Hide teleport marker
this.teleportMarker.visible = false;
if (debugState.isDataDebugEnabled()) {
logger.debug('Teleported to', { x: this.teleportPosition.x, z: this.teleportPosition.z });
}
}
// Reset teleport state
this.isTeleporting = false;
}
// Handle controller squeeze start event (grip press)
private handleControllerSqueezeStart(event: XRControllerEvent): void {
// Placeholder for future interactions
// Could be used for grabbing objects, scaling the environment, etc.
}
// Handle controller squeeze end event (grip release)
private handleControllerSqueezeEnd(event: XRControllerEvent): void {
// Placeholder for future interactions
}
// Update settings for XR
public updateSettings(settings: XRSettings): void {
this.settings = settings;
// The XRSettings interface was updated, so we access properties directly.
// We also need to check if settings itself is null before accessing its properties.
if (settings) {
this.movementSpeed = settings.movementSpeed || 1.0;
// Update floor visibility
if (settings.showFloor) {
if (!this.floorPlane) {
this.createFloor();
}
} else if (this.floorPlane) {
this.scene.remove(this.floorPlane);
this.floorPlane.geometry.dispose();
(this.floorPlane.material as THREE.Material).dispose();
this.floorPlane = null;
}
// Update teleport marker
if (settings.teleportEnabled) {
if (!this.teleportMarker) {
this.createTeleportMarker();
}
} else if (this.teleportMarker) {
this.scene.remove(this.teleportMarker);
this.teleportMarker.geometry.dispose();
(this.teleportMarker.material as THREE.Material).dispose();
this.teleportMarker = null;
}
}
}
// Clean up all XR-related resources
public dispose(): void {
// Unsubscribe from controller events
if (this.controllerSelectStartUnsubscribe) {
this.controllerSelectStartUnsubscribe();
this.controllerSelectStartUnsubscribe = null;
}
if (this.controllerSelectEndUnsubscribe) {
this.controllerSelectEndUnsubscribe();
this.controllerSelectEndUnsubscribe = null;
}
if (this.controllerSqueezeStartUnsubscribe) {
this.controllerSqueezeStartUnsubscribe();
this.controllerSqueezeStartUnsubscribe = null;
}
if (this.controllerSqueezeEndUnsubscribe) {
this.controllerSqueezeEndUnsubscribe();
this.controllerSqueezeEndUnsubscribe = null;
}
// Remove floor and teleport marker
if (this.floorPlane) {
this.scene.remove(this.floorPlane);
this.floorPlane.geometry.dispose();
(this.floorPlane.material as THREE.Material).dispose();
this.floorPlane = null;
}
if (this.teleportMarker) {
this.scene.remove(this.teleportMarker);
this.teleportMarker.geometry.dispose();
(this.teleportMarker.material as THREE.Material).dispose();
this.teleportMarker = null;
}
// Clear intersections map
this.controllerIntersections.clear();
if (debugState.isEnabled()) {
logger.info('XR initializer disposed');
}
}
}