xrblocks 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/core/Core.d.ts +2 -0
- package/build/core/Options.d.ts +8 -5
- package/build/core/components/PermissionsManager.d.ts +54 -0
- package/build/core/components/XRButton.d.ts +8 -1
- package/build/depth/DepthOptions.d.ts +8 -0
- package/build/simulator/Simulator.d.ts +2 -0
- package/build/simulator/SimulatorOptions.d.ts +5 -1
- package/build/xrblocks.js +275 -17
- package/build/xrblocks.js.map +1 -1
- package/build/xrblocks.min.js +1 -1
- package/build/xrblocks.min.js.map +1 -1
- package/package.json +1 -1
package/build/core/Core.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { XREffects } from './components/XREffects';
|
|
|
21
21
|
import { XRTransition } from './components/XRTransition';
|
|
22
22
|
import { Options } from './Options';
|
|
23
23
|
import { User } from './User';
|
|
24
|
+
import { PermissionsManager } from './components/PermissionsManager';
|
|
24
25
|
/**
|
|
25
26
|
* Core is the central engine of the XR Blocks framework, acting as a
|
|
26
27
|
* singleton manager for all XR subsystems. Its primary goal is to abstract
|
|
@@ -84,6 +85,7 @@ export declare class Core {
|
|
|
84
85
|
scriptsManager: ScriptsManager;
|
|
85
86
|
renderSceneOverride?: (renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.Camera) => void;
|
|
86
87
|
webXRSessionManager?: WebXRSessionManager;
|
|
88
|
+
permissionsManager: PermissionsManager;
|
|
87
89
|
/**
|
|
88
90
|
* Core is a singleton manager that manages all XR "blocks".
|
|
89
91
|
* It initializes core components and abstractions like the scene, camera,
|
package/build/core/Options.d.ts
CHANGED
|
@@ -108,6 +108,14 @@ export declare class Options {
|
|
|
108
108
|
showEnterSimulatorButton: boolean;
|
|
109
109
|
alwaysAutostartSimulator: boolean;
|
|
110
110
|
};
|
|
111
|
+
/**
|
|
112
|
+
* Which permissions to request before entering the XR session.
|
|
113
|
+
*/
|
|
114
|
+
permissions: {
|
|
115
|
+
geolocation: boolean;
|
|
116
|
+
camera: boolean;
|
|
117
|
+
microphone: boolean;
|
|
118
|
+
};
|
|
111
119
|
/**
|
|
112
120
|
* Constructs the Options object by merging default values with provided
|
|
113
121
|
* custom options.
|
|
@@ -161,11 +169,6 @@ export declare class Options {
|
|
|
161
169
|
* @returns The instance for chaining.
|
|
162
170
|
*/
|
|
163
171
|
enableHandRays(): this;
|
|
164
|
-
/**
|
|
165
|
-
* Enables the Gemini Live feature.
|
|
166
|
-
* @returns The instance for chaining.
|
|
167
|
-
*/
|
|
168
|
-
enableGeminiLive(): this;
|
|
169
172
|
/**
|
|
170
173
|
* Enables a standard set of AI features, including Gemini Live.
|
|
171
174
|
* @returns The instance for chaining.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface representing the result of a permission request.
|
|
3
|
+
*/
|
|
4
|
+
export interface PermissionResult {
|
|
5
|
+
granted: boolean;
|
|
6
|
+
status: PermissionState | 'unknown' | 'error';
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A utility class to manage and request browser permissions for
|
|
11
|
+
* Location, Camera, and Microphone.
|
|
12
|
+
*/
|
|
13
|
+
export declare class PermissionsManager {
|
|
14
|
+
/**
|
|
15
|
+
* Requests permission to access the user's geolocation.
|
|
16
|
+
* Note: This actually attempts to fetch the position to trigger the prompt.
|
|
17
|
+
*/
|
|
18
|
+
requestLocationPermission(): Promise<PermissionResult>;
|
|
19
|
+
/**
|
|
20
|
+
* Requests permission to access the microphone.
|
|
21
|
+
* Opens a stream to trigger the prompt, then immediately closes it.
|
|
22
|
+
*/
|
|
23
|
+
requestMicrophonePermission(): Promise<PermissionResult>;
|
|
24
|
+
/**
|
|
25
|
+
* Requests permission to access the camera.
|
|
26
|
+
* Opens a stream to trigger the prompt, then immediately closes it.
|
|
27
|
+
*/
|
|
28
|
+
requestCameraPermission(): Promise<PermissionResult>;
|
|
29
|
+
/**
|
|
30
|
+
* Requests permission for both camera and microphone simultaneously.
|
|
31
|
+
*/
|
|
32
|
+
requestAVPermission(): Promise<PermissionResult>;
|
|
33
|
+
/**
|
|
34
|
+
* Internal helper to handle getUserMedia requests.
|
|
35
|
+
* Crucially, this stops the tracks immediately after permission is granted
|
|
36
|
+
* so the hardware doesn't remain active.
|
|
37
|
+
*/
|
|
38
|
+
private requestMediaPermission;
|
|
39
|
+
/**
|
|
40
|
+
* Requests multiple permissions sequentially.
|
|
41
|
+
* Returns a single result: granted is true only if ALL requested permissions are granted.
|
|
42
|
+
*/
|
|
43
|
+
checkAndRequestPermissions({ geolocation, camera, microphone, }: {
|
|
44
|
+
geolocation?: boolean;
|
|
45
|
+
camera?: boolean;
|
|
46
|
+
microphone?: boolean;
|
|
47
|
+
}): Promise<PermissionResult>;
|
|
48
|
+
/**
|
|
49
|
+
* Checks the current status of a permission without triggering a prompt.
|
|
50
|
+
* Useful for UI state (e.g., disabling buttons if already denied).
|
|
51
|
+
* * @param permissionName - 'geolocation', 'camera', or 'microphone'
|
|
52
|
+
*/
|
|
53
|
+
checkPermissionStatus(permissionName: 'geolocation' | 'camera' | 'microphone'): Promise<PermissionState | 'unknown'>;
|
|
54
|
+
}
|
|
@@ -1,15 +1,22 @@
|
|
|
1
|
+
import { PermissionsManager } from './PermissionsManager';
|
|
1
2
|
import { WebXRSessionManager } from './WebXRSessionManager';
|
|
2
3
|
export declare class XRButton {
|
|
3
4
|
private sessionManager;
|
|
5
|
+
private permissionsManager;
|
|
4
6
|
private startText;
|
|
5
7
|
private endText;
|
|
6
8
|
private invalidText;
|
|
7
9
|
private startSimulatorText;
|
|
8
10
|
startSimulator: () => void;
|
|
11
|
+
private permissions;
|
|
9
12
|
domElement: HTMLDivElement;
|
|
10
13
|
simulatorButtonElement: HTMLButtonElement;
|
|
11
14
|
xrButtonElement: HTMLButtonElement;
|
|
12
|
-
constructor(sessionManager: WebXRSessionManager, startText?: string, endText?: string, invalidText?: string, startSimulatorText?: string, showEnterSimulatorButton?: boolean, startSimulator?: () => void
|
|
15
|
+
constructor(sessionManager: WebXRSessionManager, permissionsManager: PermissionsManager, startText?: string, endText?: string, invalidText?: string, startSimulatorText?: string, showEnterSimulatorButton?: boolean, startSimulator?: () => void, permissions?: {
|
|
16
|
+
geolocation: boolean;
|
|
17
|
+
camera: boolean;
|
|
18
|
+
microphone: boolean;
|
|
19
|
+
});
|
|
13
20
|
private createSimulatorButton;
|
|
14
21
|
private createXRButtonElement;
|
|
15
22
|
private onSessionReady;
|
|
@@ -28,6 +28,8 @@ export declare class DepthOptions {
|
|
|
28
28
|
enabled: boolean;
|
|
29
29
|
};
|
|
30
30
|
useFloat32: boolean;
|
|
31
|
+
depthTypeRequest: XRDepthType[];
|
|
32
|
+
matchDepthView: boolean;
|
|
31
33
|
constructor(options?: DeepReadonly<DeepPartial<DepthOptions>>);
|
|
32
34
|
}
|
|
33
35
|
export declare const xrDepthMeshOptions: {
|
|
@@ -58,6 +60,8 @@ export declare const xrDepthMeshOptions: {
|
|
|
58
60
|
readonly enabled: boolean;
|
|
59
61
|
};
|
|
60
62
|
readonly useFloat32: boolean;
|
|
63
|
+
readonly depthTypeRequest: readonly XRDepthType[];
|
|
64
|
+
readonly matchDepthView: boolean;
|
|
61
65
|
};
|
|
62
66
|
export declare const xrDepthMeshVisualizationOptions: {
|
|
63
67
|
readonly debugging: boolean;
|
|
@@ -87,6 +91,8 @@ export declare const xrDepthMeshVisualizationOptions: {
|
|
|
87
91
|
readonly enabled: boolean;
|
|
88
92
|
};
|
|
89
93
|
readonly useFloat32: boolean;
|
|
94
|
+
readonly depthTypeRequest: readonly XRDepthType[];
|
|
95
|
+
readonly matchDepthView: boolean;
|
|
90
96
|
};
|
|
91
97
|
export declare const xrDepthMeshPhysicsOptions: {
|
|
92
98
|
readonly debugging: boolean;
|
|
@@ -116,4 +122,6 @@ export declare const xrDepthMeshPhysicsOptions: {
|
|
|
116
122
|
readonly enabled: boolean;
|
|
117
123
|
};
|
|
118
124
|
readonly useFloat32: boolean;
|
|
125
|
+
readonly depthTypeRequest: readonly XRDepthType[];
|
|
126
|
+
readonly matchDepthView: boolean;
|
|
119
127
|
};
|
|
@@ -42,6 +42,8 @@ export declare class Simulator extends Script {
|
|
|
42
42
|
effects?: XREffects;
|
|
43
43
|
virtualSceneRenderTarget?: THREE.WebGLRenderTarget;
|
|
44
44
|
virtualSceneFullScreenQuad?: FullScreenQuad;
|
|
45
|
+
backgroundVideoQuad?: FullScreenQuad;
|
|
46
|
+
videoElement?: HTMLVideoElement;
|
|
45
47
|
camera?: SimulatorCamera;
|
|
46
48
|
options: SimulatorOptions;
|
|
47
49
|
renderer: THREE.WebGLRenderer;
|
|
@@ -23,6 +23,7 @@ export declare class SimulatorOptions {
|
|
|
23
23
|
z: number;
|
|
24
24
|
};
|
|
25
25
|
scenePath: string;
|
|
26
|
+
videoPath?: string;
|
|
26
27
|
initialScenePosition: {
|
|
27
28
|
x: number;
|
|
28
29
|
y: number;
|
|
@@ -43,7 +44,10 @@ export declare class SimulatorOptions {
|
|
|
43
44
|
enabled: boolean;
|
|
44
45
|
element: string;
|
|
45
46
|
};
|
|
46
|
-
|
|
47
|
+
geminiLivePanel: {
|
|
48
|
+
enabled: boolean;
|
|
49
|
+
element: string;
|
|
50
|
+
};
|
|
47
51
|
stereo: {
|
|
48
52
|
enabled: boolean;
|
|
49
53
|
};
|
package/build/xrblocks.js
CHANGED
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*
|
|
16
16
|
* @file xrblocks.js
|
|
17
|
-
* @version v0.
|
|
18
|
-
* @commitid
|
|
19
|
-
* @builddate 2025-
|
|
17
|
+
* @version v0.5.0
|
|
18
|
+
* @commitid c2f4b09
|
|
19
|
+
* @builddate 2025-12-04T15:14:30.184Z
|
|
20
20
|
* @description XR Blocks SDK, built from source with the above commit ID.
|
|
21
21
|
* @agent When using with Gemini to create XR apps, use **Gemini Canvas** mode,
|
|
22
22
|
* and follow rules below:
|
|
@@ -2408,6 +2408,8 @@ class DepthOptions {
|
|
|
2408
2408
|
// Occlusion pass.
|
|
2409
2409
|
this.occlusion = { enabled: false };
|
|
2410
2410
|
this.useFloat32 = true;
|
|
2411
|
+
this.depthTypeRequest = ['raw'];
|
|
2412
|
+
this.matchDepthView = true;
|
|
2411
2413
|
deepMerge(this, options);
|
|
2412
2414
|
}
|
|
2413
2415
|
}
|
|
@@ -4402,13 +4404,19 @@ class WebXRSessionManager extends THREE.EventDispatcher {
|
|
|
4402
4404
|
const XRBUTTON_WRAPPER_ID = 'XRButtonWrapper';
|
|
4403
4405
|
const XRBUTTON_CLASS = 'XRButton';
|
|
4404
4406
|
class XRButton {
|
|
4405
|
-
constructor(sessionManager, startText = 'ENTER XR', endText = 'END XR', invalidText = 'XR NOT SUPPORTED', startSimulatorText = 'START SIMULATOR', showEnterSimulatorButton = false, startSimulator = () => { }
|
|
4407
|
+
constructor(sessionManager, permissionsManager, startText = 'ENTER XR', endText = 'END XR', invalidText = 'XR NOT SUPPORTED', startSimulatorText = 'START SIMULATOR', showEnterSimulatorButton = false, startSimulator = () => { }, permissions = {
|
|
4408
|
+
geolocation: false,
|
|
4409
|
+
camera: false,
|
|
4410
|
+
microphone: false,
|
|
4411
|
+
}) {
|
|
4406
4412
|
this.sessionManager = sessionManager;
|
|
4413
|
+
this.permissionsManager = permissionsManager;
|
|
4407
4414
|
this.startText = startText;
|
|
4408
4415
|
this.endText = endText;
|
|
4409
4416
|
this.invalidText = invalidText;
|
|
4410
4417
|
this.startSimulatorText = startSimulatorText;
|
|
4411
4418
|
this.startSimulator = startSimulator;
|
|
4419
|
+
this.permissions = permissions;
|
|
4412
4420
|
this.domElement = document.createElement('div');
|
|
4413
4421
|
this.simulatorButtonElement = document.createElement('button');
|
|
4414
4422
|
this.xrButtonElement = document.createElement('button');
|
|
@@ -4443,7 +4451,17 @@ class XRButton {
|
|
|
4443
4451
|
button.innerHTML = this.startText;
|
|
4444
4452
|
button.disabled = false;
|
|
4445
4453
|
button.onclick = () => {
|
|
4446
|
-
this.
|
|
4454
|
+
this.permissionsManager
|
|
4455
|
+
.checkAndRequestPermissions(this.permissions)
|
|
4456
|
+
.then((result) => {
|
|
4457
|
+
if (result.granted) {
|
|
4458
|
+
this.sessionManager.startSession();
|
|
4459
|
+
}
|
|
4460
|
+
else {
|
|
4461
|
+
this.xrButtonElement.textContent =
|
|
4462
|
+
'Error:' + result.error + '\nPlease try again.';
|
|
4463
|
+
}
|
|
4464
|
+
});
|
|
4447
4465
|
};
|
|
4448
4466
|
}
|
|
4449
4467
|
showXRNotSupported() {
|
|
@@ -7172,6 +7190,7 @@ class SimulatorOptions {
|
|
|
7172
7190
|
constructor(options) {
|
|
7173
7191
|
this.initialCameraPosition = { x: 0, y: 1.5, z: 0 };
|
|
7174
7192
|
this.scenePath = XR_BLOCKS_ASSETS_PATH + 'simulator/scenes/XREmulatorsceneV5_livingRoom.glb';
|
|
7193
|
+
this.videoPath = undefined;
|
|
7175
7194
|
this.initialScenePosition = { x: -1.6, y: 0.3, z: 0 };
|
|
7176
7195
|
this.defaultMode = SimulatorMode.USER;
|
|
7177
7196
|
this.defaultHand = Handedness.LEFT;
|
|
@@ -7188,7 +7207,10 @@ class SimulatorOptions {
|
|
|
7188
7207
|
enabled: true,
|
|
7189
7208
|
element: 'xrblocks-simulator-hand-pose-panel',
|
|
7190
7209
|
};
|
|
7191
|
-
this.
|
|
7210
|
+
this.geminiLivePanel = {
|
|
7211
|
+
enabled: false,
|
|
7212
|
+
element: 'xrblocks-simulator-geminilive',
|
|
7213
|
+
};
|
|
7192
7214
|
this.stereo = {
|
|
7193
7215
|
enabled: false,
|
|
7194
7216
|
};
|
|
@@ -7436,6 +7458,14 @@ class Options {
|
|
|
7436
7458
|
// Whether to autostart the simulator even if WebXR is available.
|
|
7437
7459
|
alwaysAutostartSimulator: false,
|
|
7438
7460
|
};
|
|
7461
|
+
/**
|
|
7462
|
+
* Which permissions to request before entering the XR session.
|
|
7463
|
+
*/
|
|
7464
|
+
this.permissions = {
|
|
7465
|
+
geolocation: false,
|
|
7466
|
+
camera: false,
|
|
7467
|
+
microphone: false,
|
|
7468
|
+
};
|
|
7439
7469
|
deepMerge(this, options);
|
|
7440
7470
|
}
|
|
7441
7471
|
/**
|
|
@@ -7476,6 +7506,7 @@ class Options {
|
|
|
7476
7506
|
* @returns The instance for chaining.
|
|
7477
7507
|
*/
|
|
7478
7508
|
enableObjectDetection() {
|
|
7509
|
+
this.permissions.camera = true;
|
|
7479
7510
|
this.world.enableObjectDetection();
|
|
7480
7511
|
return this;
|
|
7481
7512
|
}
|
|
@@ -7486,6 +7517,7 @@ class Options {
|
|
|
7486
7517
|
* @returns The instance for chaining.
|
|
7487
7518
|
*/
|
|
7488
7519
|
enableCamera(facingMode = 'environment') {
|
|
7520
|
+
this.permissions.camera = true;
|
|
7489
7521
|
this.deviceCamera = new DeviceCameraOptions(facingMode === 'environment'
|
|
7490
7522
|
? xrDeviceCameraEnvironmentOptions
|
|
7491
7523
|
: xrDeviceCameraUserOptions);
|
|
@@ -7516,14 +7548,6 @@ class Options {
|
|
|
7516
7548
|
this.controllers.visualizeRays = true;
|
|
7517
7549
|
return this;
|
|
7518
7550
|
}
|
|
7519
|
-
/**
|
|
7520
|
-
* Enables the Gemini Live feature.
|
|
7521
|
-
* @returns The instance for chaining.
|
|
7522
|
-
*/
|
|
7523
|
-
enableGeminiLive() {
|
|
7524
|
-
this.simulator.geminilive = true;
|
|
7525
|
-
return this;
|
|
7526
|
-
}
|
|
7527
7551
|
/**
|
|
7528
7552
|
* Enables a standard set of AI features, including Gemini Live.
|
|
7529
7553
|
* @returns The instance for chaining.
|
|
@@ -9785,8 +9809,8 @@ class SimulatorInterface {
|
|
|
9785
9809
|
}
|
|
9786
9810
|
}
|
|
9787
9811
|
showGeminiLivePanel(simulatorOptions) {
|
|
9788
|
-
if (simulatorOptions.
|
|
9789
|
-
const element = document.createElement(
|
|
9812
|
+
if (simulatorOptions.geminiLivePanel.enabled) {
|
|
9813
|
+
const element = document.createElement(simulatorOptions.geminiLivePanel.element);
|
|
9790
9814
|
document.body.appendChild(element);
|
|
9791
9815
|
this.elements.push(element);
|
|
9792
9816
|
}
|
|
@@ -9830,6 +9854,9 @@ class SimulatorScene extends THREE.Scene {
|
|
|
9830
9854
|
}
|
|
9831
9855
|
async init(simulatorOptions) {
|
|
9832
9856
|
this.addLights();
|
|
9857
|
+
if (simulatorOptions.videoPath) {
|
|
9858
|
+
return;
|
|
9859
|
+
}
|
|
9833
9860
|
if (simulatorOptions.scenePath) {
|
|
9834
9861
|
await this.loadGLTF(simulatorOptions.scenePath, new THREE.Vector3(simulatorOptions.initialScenePosition.x, simulatorOptions.initialScenePosition.y, simulatorOptions.initialScenePosition.z));
|
|
9835
9862
|
}
|
|
@@ -9967,6 +9994,21 @@ class Simulator extends Script {
|
|
|
9967
9994
|
if (this.options.stereo.enabled) {
|
|
9968
9995
|
this.setupStereoCameras(camera);
|
|
9969
9996
|
}
|
|
9997
|
+
if (this.options.videoPath) {
|
|
9998
|
+
this.videoElement = document.createElement('video');
|
|
9999
|
+
this.videoElement.src = this.options.videoPath;
|
|
10000
|
+
this.videoElement.loop = true;
|
|
10001
|
+
this.videoElement.muted = true;
|
|
10002
|
+
this.videoElement.play().catch((e) => {
|
|
10003
|
+
console.error(`Simulator: Failed to play video at ${this.options.videoPath}`, e);
|
|
10004
|
+
});
|
|
10005
|
+
this.videoElement.addEventListener('error', () => {
|
|
10006
|
+
console.error(`Simulator: Error loading video at ${this.options.videoPath}`, this.videoElement?.error);
|
|
10007
|
+
});
|
|
10008
|
+
const videoTexture = new THREE.VideoTexture(this.videoElement);
|
|
10009
|
+
videoTexture.colorSpace = THREE.SRGBColorSpace;
|
|
10010
|
+
this.backgroundVideoQuad = new FullScreenQuad(new THREE.MeshBasicMaterial({ map: videoTexture }));
|
|
10011
|
+
}
|
|
9970
10012
|
this.virtualSceneRenderTarget = new THREE.WebGLRenderTarget(renderer.domElement.width, renderer.domElement.height, { stencilBuffer: options.stencil });
|
|
9971
10013
|
const virtualSceneMaterial = new THREE.MeshBasicMaterial({
|
|
9972
10014
|
map: this.virtualSceneRenderTarget.texture,
|
|
@@ -10076,6 +10118,9 @@ class Simulator extends Script {
|
|
|
10076
10118
|
this.sparkRenderer.defaultView.encodeLinear = false;
|
|
10077
10119
|
}
|
|
10078
10120
|
this.renderer.setRenderTarget(null);
|
|
10121
|
+
if (this.backgroundVideoQuad) {
|
|
10122
|
+
this.backgroundVideoQuad.render(this.renderer);
|
|
10123
|
+
}
|
|
10079
10124
|
this.renderer.render(this.simulatorScene, camera);
|
|
10080
10125
|
this.renderer.clearDepth();
|
|
10081
10126
|
}
|
|
@@ -14397,6 +14442,216 @@ class XRTransition extends MeshScript {
|
|
|
14397
14442
|
}
|
|
14398
14443
|
}
|
|
14399
14444
|
|
|
14445
|
+
/**
|
|
14446
|
+
* A utility class to manage and request browser permissions for
|
|
14447
|
+
* Location, Camera, and Microphone.
|
|
14448
|
+
*/
|
|
14449
|
+
class PermissionsManager {
|
|
14450
|
+
/**
|
|
14451
|
+
* Requests permission to access the user's geolocation.
|
|
14452
|
+
* Note: This actually attempts to fetch the position to trigger the prompt.
|
|
14453
|
+
*/
|
|
14454
|
+
async requestLocationPermission() {
|
|
14455
|
+
if (!('geolocation' in navigator)) {
|
|
14456
|
+
return {
|
|
14457
|
+
granted: false,
|
|
14458
|
+
status: 'error',
|
|
14459
|
+
error: 'Geolocation is not supported by this browser.',
|
|
14460
|
+
};
|
|
14461
|
+
}
|
|
14462
|
+
return new Promise((resolve) => {
|
|
14463
|
+
navigator.geolocation.getCurrentPosition(() => {
|
|
14464
|
+
resolve({ granted: true, status: 'granted' });
|
|
14465
|
+
}, (error) => {
|
|
14466
|
+
let errorMsg = 'Unknown error';
|
|
14467
|
+
switch (error.code) {
|
|
14468
|
+
case error.PERMISSION_DENIED:
|
|
14469
|
+
errorMsg = 'User denied the request.';
|
|
14470
|
+
break;
|
|
14471
|
+
case error.POSITION_UNAVAILABLE:
|
|
14472
|
+
errorMsg = 'Location information is unavailable.';
|
|
14473
|
+
break;
|
|
14474
|
+
case error.TIMEOUT:
|
|
14475
|
+
errorMsg = 'The request to get user location timed out.';
|
|
14476
|
+
break;
|
|
14477
|
+
}
|
|
14478
|
+
resolve({ granted: false, status: 'denied', error: errorMsg });
|
|
14479
|
+
}, { timeout: 10000 } // 10 second timeout
|
|
14480
|
+
);
|
|
14481
|
+
});
|
|
14482
|
+
}
|
|
14483
|
+
/**
|
|
14484
|
+
* Requests permission to access the microphone.
|
|
14485
|
+
* Opens a stream to trigger the prompt, then immediately closes it.
|
|
14486
|
+
*/
|
|
14487
|
+
async requestMicrophonePermission() {
|
|
14488
|
+
return this.requestMediaPermission({ audio: true });
|
|
14489
|
+
}
|
|
14490
|
+
/**
|
|
14491
|
+
* Requests permission to access the camera.
|
|
14492
|
+
* Opens a stream to trigger the prompt, then immediately closes it.
|
|
14493
|
+
*/
|
|
14494
|
+
async requestCameraPermission() {
|
|
14495
|
+
return this.requestMediaPermission({ video: true });
|
|
14496
|
+
}
|
|
14497
|
+
/**
|
|
14498
|
+
* Requests permission for both camera and microphone simultaneously.
|
|
14499
|
+
*/
|
|
14500
|
+
async requestAVPermission() {
|
|
14501
|
+
return this.requestMediaPermission({ video: true, audio: true });
|
|
14502
|
+
}
|
|
14503
|
+
/**
|
|
14504
|
+
* Internal helper to handle getUserMedia requests.
|
|
14505
|
+
* Crucially, this stops the tracks immediately after permission is granted
|
|
14506
|
+
* so the hardware doesn't remain active.
|
|
14507
|
+
*/
|
|
14508
|
+
async requestMediaPermission(constraints) {
|
|
14509
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
14510
|
+
return {
|
|
14511
|
+
granted: false,
|
|
14512
|
+
status: 'error',
|
|
14513
|
+
error: 'Media Devices API is not supported by this browser.',
|
|
14514
|
+
};
|
|
14515
|
+
}
|
|
14516
|
+
try {
|
|
14517
|
+
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
14518
|
+
// Permission granted. Now stop the stream to release hardware.
|
|
14519
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
14520
|
+
return { granted: true, status: 'granted' };
|
|
14521
|
+
}
|
|
14522
|
+
catch (err) {
|
|
14523
|
+
// Handle common getUserMedia errors
|
|
14524
|
+
const status = 'denied';
|
|
14525
|
+
let errorMessage = 'Permission denied';
|
|
14526
|
+
if (err instanceof Error) {
|
|
14527
|
+
if (err.name === 'NotFoundError' ||
|
|
14528
|
+
err.name === 'DevicesNotFoundError') {
|
|
14529
|
+
return {
|
|
14530
|
+
granted: false,
|
|
14531
|
+
status: 'error',
|
|
14532
|
+
error: 'Hardware not found.',
|
|
14533
|
+
};
|
|
14534
|
+
}
|
|
14535
|
+
errorMessage = err.message || errorMessage;
|
|
14536
|
+
}
|
|
14537
|
+
return { granted: false, status: status, error: errorMessage };
|
|
14538
|
+
}
|
|
14539
|
+
}
|
|
14540
|
+
/**
|
|
14541
|
+
* Requests multiple permissions sequentially.
|
|
14542
|
+
* Returns a single result: granted is true only if ALL requested permissions are granted.
|
|
14543
|
+
*/
|
|
14544
|
+
async checkAndRequestPermissions({ geolocation = false, camera = false, microphone = false, }) {
|
|
14545
|
+
const results = [];
|
|
14546
|
+
// 1. Handle Location
|
|
14547
|
+
if (geolocation) {
|
|
14548
|
+
const status = await this.checkPermissionStatus('geolocation');
|
|
14549
|
+
if (status === 'granted') {
|
|
14550
|
+
results.push({ granted: true, status: 'granted' });
|
|
14551
|
+
}
|
|
14552
|
+
else {
|
|
14553
|
+
results.push(await this.requestLocationPermission());
|
|
14554
|
+
}
|
|
14555
|
+
}
|
|
14556
|
+
// 2. Handle Media (Camera & Mic)
|
|
14557
|
+
// We group these because requestAVPermission can ask for both in one prompt
|
|
14558
|
+
if (camera && microphone) {
|
|
14559
|
+
const camStatus = await this.checkPermissionStatus('camera');
|
|
14560
|
+
const micStatus = await this.checkPermissionStatus('microphone');
|
|
14561
|
+
if (camStatus === 'granted' && micStatus === 'granted') {
|
|
14562
|
+
results.push({ granted: true, status: 'granted' });
|
|
14563
|
+
}
|
|
14564
|
+
else if (camStatus === 'granted') {
|
|
14565
|
+
// Only need mic
|
|
14566
|
+
results.push(await this.requestMicrophonePermission());
|
|
14567
|
+
}
|
|
14568
|
+
else if (micStatus === 'granted') {
|
|
14569
|
+
// Only need camera
|
|
14570
|
+
results.push(await this.requestCameraPermission());
|
|
14571
|
+
}
|
|
14572
|
+
else {
|
|
14573
|
+
// Need both
|
|
14574
|
+
results.push(await this.requestAVPermission());
|
|
14575
|
+
}
|
|
14576
|
+
}
|
|
14577
|
+
else if (camera) {
|
|
14578
|
+
const status = await this.checkPermissionStatus('camera');
|
|
14579
|
+
if (status === 'granted') {
|
|
14580
|
+
results.push({ granted: true, status: 'granted' });
|
|
14581
|
+
}
|
|
14582
|
+
else {
|
|
14583
|
+
results.push(await this.requestCameraPermission());
|
|
14584
|
+
}
|
|
14585
|
+
}
|
|
14586
|
+
else if (microphone) {
|
|
14587
|
+
const status = await this.checkPermissionStatus('microphone');
|
|
14588
|
+
if (status === 'granted') {
|
|
14589
|
+
results.push({ granted: true, status: 'granted' });
|
|
14590
|
+
}
|
|
14591
|
+
else {
|
|
14592
|
+
results.push(await this.requestMicrophonePermission());
|
|
14593
|
+
}
|
|
14594
|
+
}
|
|
14595
|
+
// 3. Aggregate results
|
|
14596
|
+
if (results.length === 0) {
|
|
14597
|
+
return { granted: true, status: 'granted' };
|
|
14598
|
+
}
|
|
14599
|
+
const allGranted = results.every((r) => r.granted);
|
|
14600
|
+
const anyDenied = results.find((r) => r.status === 'denied');
|
|
14601
|
+
const anyError = results.find((r) => r.status === 'error');
|
|
14602
|
+
// Aggregate errors
|
|
14603
|
+
const errors = results
|
|
14604
|
+
.filter((r) => r.error)
|
|
14605
|
+
.map((r) => r.error)
|
|
14606
|
+
.join(' | ');
|
|
14607
|
+
let finalStatus = 'granted';
|
|
14608
|
+
if (anyError)
|
|
14609
|
+
finalStatus = 'error';
|
|
14610
|
+
else if (anyDenied)
|
|
14611
|
+
finalStatus = 'denied';
|
|
14612
|
+
return {
|
|
14613
|
+
granted: allGranted,
|
|
14614
|
+
status: finalStatus,
|
|
14615
|
+
error: errors || undefined,
|
|
14616
|
+
};
|
|
14617
|
+
}
|
|
14618
|
+
/**
|
|
14619
|
+
* Checks the current status of a permission without triggering a prompt.
|
|
14620
|
+
* Useful for UI state (e.g., disabling buttons if already denied).
|
|
14621
|
+
* * @param permissionName - 'geolocation', 'camera', or 'microphone'
|
|
14622
|
+
*/
|
|
14623
|
+
async checkPermissionStatus(permissionName) {
|
|
14624
|
+
if (!navigator.permissions || !navigator.permissions.query) {
|
|
14625
|
+
return 'unknown';
|
|
14626
|
+
}
|
|
14627
|
+
try {
|
|
14628
|
+
let queryName;
|
|
14629
|
+
// Map friendly names to API PermissionName types
|
|
14630
|
+
// Note: 'camera' and 'microphone' are part of the newer spec,
|
|
14631
|
+
// but strictly Typed TypeScript might expect specific descriptor objects.
|
|
14632
|
+
if (permissionName === 'geolocation') {
|
|
14633
|
+
queryName = 'geolocation';
|
|
14634
|
+
}
|
|
14635
|
+
else if (permissionName === 'camera' ||
|
|
14636
|
+
permissionName === 'microphone') {
|
|
14637
|
+
const descriptor = { name: permissionName };
|
|
14638
|
+
const result = await navigator.permissions.query(descriptor);
|
|
14639
|
+
return result.state;
|
|
14640
|
+
}
|
|
14641
|
+
else {
|
|
14642
|
+
return 'unknown';
|
|
14643
|
+
}
|
|
14644
|
+
const result = await navigator.permissions.query({ name: queryName });
|
|
14645
|
+
return result.state;
|
|
14646
|
+
}
|
|
14647
|
+
catch (error) {
|
|
14648
|
+
// Firefox and Safari have incomplete Permissions API support
|
|
14649
|
+
console.warn(`Error checking permission status for ${permissionName}`, error);
|
|
14650
|
+
return 'unknown';
|
|
14651
|
+
}
|
|
14652
|
+
}
|
|
14653
|
+
}
|
|
14654
|
+
|
|
14400
14655
|
/**
|
|
14401
14656
|
* Core is the central engine of the XR Blocks framework, acting as a
|
|
14402
14657
|
* singleton manager for all XR subsystems. Its primary goal is to abstract
|
|
@@ -14456,6 +14711,7 @@ class Core {
|
|
|
14456
14711
|
await script.initPhysics(this.physics);
|
|
14457
14712
|
}
|
|
14458
14713
|
});
|
|
14714
|
+
this.permissionsManager = new PermissionsManager();
|
|
14459
14715
|
if (Core.instance) {
|
|
14460
14716
|
return Core.instance;
|
|
14461
14717
|
}
|
|
@@ -14564,6 +14820,8 @@ class Core {
|
|
|
14564
14820
|
dataFormatPreference: [
|
|
14565
14821
|
this.options.depth.useFloat32 ? 'float32' : 'luminance-alpha',
|
|
14566
14822
|
],
|
|
14823
|
+
depthTypeRequest: options.depth.depthTypeRequest,
|
|
14824
|
+
matchDepthView: options.depth.matchDepthView,
|
|
14567
14825
|
};
|
|
14568
14826
|
this.depth.init(this.camera, options.depth, this.renderer, this.registry, this.scene);
|
|
14569
14827
|
}
|
|
@@ -14600,7 +14858,7 @@ class Core {
|
|
|
14600
14858
|
// Sets up xrButton.
|
|
14601
14859
|
let shouldAutostartSimulator = this.options.xrButton.alwaysAutostartSimulator;
|
|
14602
14860
|
if (!shouldAutostartSimulator && options.xrButton.enabled) {
|
|
14603
|
-
this.xrButton = new XRButton(this.webXRSessionManager, options.xrButton?.startText, options.xrButton?.endText, options.xrButton?.invalidText, options.xrButton?.startSimulatorText, options.xrButton?.showEnterSimulatorButton, this.startSimulator.bind(this));
|
|
14861
|
+
this.xrButton = new XRButton(this.webXRSessionManager, this.permissionsManager, options.xrButton?.startText, options.xrButton?.endText, options.xrButton?.invalidText, options.xrButton?.startSimulatorText, options.xrButton?.showEnterSimulatorButton, this.startSimulator.bind(this), options.permissions);
|
|
14604
14862
|
document.body.appendChild(this.xrButton.domElement);
|
|
14605
14863
|
}
|
|
14606
14864
|
this.webXRSessionManager.addEventListener(WebXRSessionEventType.UNSUPPORTED, () => {
|