xrblocks 0.3.1 → 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/addons/ai/GeminiManager.d.ts +1 -0
- package/build/addons/ai/GeminiManager.js +3 -2
- package/build/core/Core.d.ts +2 -0
- package/build/core/Options.d.ts +10 -6
- package/build/core/components/PermissionsManager.d.ts +54 -0
- package/build/core/components/WebXRSessionManager.d.ts +1 -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/sound/AudioPlayer.d.ts +1 -0
- package/build/xrblocks.js +294 -26
- 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/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
|
}
|
|
@@ -3208,9 +3210,9 @@ class Depth {
|
|
|
3208
3210
|
update(frame) {
|
|
3209
3211
|
if (!this.options.enabled)
|
|
3210
3212
|
return;
|
|
3211
|
-
if (
|
|
3212
|
-
|
|
3213
|
-
|
|
3213
|
+
if (frame) {
|
|
3214
|
+
this.updateLocalDepth(frame);
|
|
3215
|
+
}
|
|
3214
3216
|
if (this.options.occlusion.enabled) {
|
|
3215
3217
|
this.renderOcclusionPass();
|
|
3216
3218
|
}
|
|
@@ -4284,6 +4286,7 @@ class WebXRSessionManager extends THREE.EventDispatcher {
|
|
|
4284
4286
|
this.sessionInit = sessionInit;
|
|
4285
4287
|
this.mode = mode;
|
|
4286
4288
|
this.onSessionEndedBound = this.onSessionEndedInternal.bind(this);
|
|
4289
|
+
this.waitingForXRSession = false;
|
|
4287
4290
|
}
|
|
4288
4291
|
/**
|
|
4289
4292
|
* Checks for WebXR support and availability of the requested session mode.
|
|
@@ -4350,8 +4353,15 @@ class WebXRSessionManager extends THREE.EventDispatcher {
|
|
|
4350
4353
|
else if (this.currentSession) {
|
|
4351
4354
|
throw new Error('Session already started');
|
|
4352
4355
|
}
|
|
4356
|
+
else if (this.waitingForXRSession) {
|
|
4357
|
+
throw new Error('Waiting for session to start');
|
|
4358
|
+
}
|
|
4359
|
+
this.waitingForXRSession = true;
|
|
4353
4360
|
navigator
|
|
4354
4361
|
.xr.requestSession(this.mode, this.sessionOptions)
|
|
4362
|
+
.finally(() => {
|
|
4363
|
+
this.waitingForXRSession = false;
|
|
4364
|
+
})
|
|
4355
4365
|
.then(this.onSessionStartedInternal.bind(this));
|
|
4356
4366
|
}
|
|
4357
4367
|
/**
|
|
@@ -4394,19 +4404,25 @@ class WebXRSessionManager extends THREE.EventDispatcher {
|
|
|
4394
4404
|
const XRBUTTON_WRAPPER_ID = 'XRButtonWrapper';
|
|
4395
4405
|
const XRBUTTON_CLASS = 'XRButton';
|
|
4396
4406
|
class XRButton {
|
|
4397
|
-
constructor(sessionManager, startText = 'ENTER XR', endText = 'END XR', invalidText = 'XR NOT SUPPORTED', startSimulatorText = 'START SIMULATOR',
|
|
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
|
+
}) {
|
|
4398
4412
|
this.sessionManager = sessionManager;
|
|
4413
|
+
this.permissionsManager = permissionsManager;
|
|
4399
4414
|
this.startText = startText;
|
|
4400
4415
|
this.endText = endText;
|
|
4401
4416
|
this.invalidText = invalidText;
|
|
4402
4417
|
this.startSimulatorText = startSimulatorText;
|
|
4403
4418
|
this.startSimulator = startSimulator;
|
|
4419
|
+
this.permissions = permissions;
|
|
4404
4420
|
this.domElement = document.createElement('div');
|
|
4405
4421
|
this.simulatorButtonElement = document.createElement('button');
|
|
4406
4422
|
this.xrButtonElement = document.createElement('button');
|
|
4407
4423
|
this.domElement.id = XRBUTTON_WRAPPER_ID;
|
|
4408
4424
|
this.createXRButtonElement();
|
|
4409
|
-
if (
|
|
4425
|
+
if (showEnterSimulatorButton) {
|
|
4410
4426
|
this.createSimulatorButton();
|
|
4411
4427
|
}
|
|
4412
4428
|
this.sessionManager.addEventListener(WebXRSessionEventType.UNSUPPORTED, this.showXRNotSupported.bind(this));
|
|
@@ -4435,7 +4451,17 @@ class XRButton {
|
|
|
4435
4451
|
button.innerHTML = this.startText;
|
|
4436
4452
|
button.disabled = false;
|
|
4437
4453
|
button.onclick = () => {
|
|
4438
|
-
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
|
+
});
|
|
4439
4465
|
};
|
|
4440
4466
|
}
|
|
4441
4467
|
showXRNotSupported() {
|
|
@@ -7164,6 +7190,7 @@ class SimulatorOptions {
|
|
|
7164
7190
|
constructor(options) {
|
|
7165
7191
|
this.initialCameraPosition = { x: 0, y: 1.5, z: 0 };
|
|
7166
7192
|
this.scenePath = XR_BLOCKS_ASSETS_PATH + 'simulator/scenes/XREmulatorsceneV5_livingRoom.glb';
|
|
7193
|
+
this.videoPath = undefined;
|
|
7167
7194
|
this.initialScenePosition = { x: -1.6, y: 0.3, z: 0 };
|
|
7168
7195
|
this.defaultMode = SimulatorMode.USER;
|
|
7169
7196
|
this.defaultHand = Handedness.LEFT;
|
|
@@ -7180,7 +7207,10 @@ class SimulatorOptions {
|
|
|
7180
7207
|
enabled: true,
|
|
7181
7208
|
element: 'xrblocks-simulator-hand-pose-panel',
|
|
7182
7209
|
};
|
|
7183
|
-
this.
|
|
7210
|
+
this.geminiLivePanel = {
|
|
7211
|
+
enabled: false,
|
|
7212
|
+
element: 'xrblocks-simulator-geminilive',
|
|
7213
|
+
};
|
|
7184
7214
|
this.stereo = {
|
|
7185
7215
|
enabled: false,
|
|
7186
7216
|
};
|
|
@@ -7414,6 +7444,7 @@ class Options {
|
|
|
7414
7444
|
* Whether to use post-processing effects.
|
|
7415
7445
|
*/
|
|
7416
7446
|
this.usePostprocessing = false;
|
|
7447
|
+
this.enableSimulator = true;
|
|
7417
7448
|
/**
|
|
7418
7449
|
* Configuration for the XR session button.
|
|
7419
7450
|
*/
|
|
@@ -7423,10 +7454,18 @@ class Options {
|
|
|
7423
7454
|
endText: 'Exit XR',
|
|
7424
7455
|
invalidText: 'XR Not Supported',
|
|
7425
7456
|
startSimulatorText: 'Enter Simulator',
|
|
7426
|
-
|
|
7457
|
+
showEnterSimulatorButton: false,
|
|
7427
7458
|
// Whether to autostart the simulator even if WebXR is available.
|
|
7428
7459
|
alwaysAutostartSimulator: false,
|
|
7429
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
|
+
};
|
|
7430
7469
|
deepMerge(this, options);
|
|
7431
7470
|
}
|
|
7432
7471
|
/**
|
|
@@ -7467,6 +7506,7 @@ class Options {
|
|
|
7467
7506
|
* @returns The instance for chaining.
|
|
7468
7507
|
*/
|
|
7469
7508
|
enableObjectDetection() {
|
|
7509
|
+
this.permissions.camera = true;
|
|
7470
7510
|
this.world.enableObjectDetection();
|
|
7471
7511
|
return this;
|
|
7472
7512
|
}
|
|
@@ -7477,6 +7517,7 @@ class Options {
|
|
|
7477
7517
|
* @returns The instance for chaining.
|
|
7478
7518
|
*/
|
|
7479
7519
|
enableCamera(facingMode = 'environment') {
|
|
7520
|
+
this.permissions.camera = true;
|
|
7480
7521
|
this.deviceCamera = new DeviceCameraOptions(facingMode === 'environment'
|
|
7481
7522
|
? xrDeviceCameraEnvironmentOptions
|
|
7482
7523
|
: xrDeviceCameraUserOptions);
|
|
@@ -7507,14 +7548,6 @@ class Options {
|
|
|
7507
7548
|
this.controllers.visualizeRays = true;
|
|
7508
7549
|
return this;
|
|
7509
7550
|
}
|
|
7510
|
-
/**
|
|
7511
|
-
* Enables the Gemini Live feature.
|
|
7512
|
-
* @returns The instance for chaining.
|
|
7513
|
-
*/
|
|
7514
|
-
enableGeminiLive() {
|
|
7515
|
-
this.simulator.geminilive = true;
|
|
7516
|
-
return this;
|
|
7517
|
-
}
|
|
7518
7551
|
/**
|
|
7519
7552
|
* Enables a standard set of AI features, including Gemini Live.
|
|
7520
7553
|
* @returns The instance for chaining.
|
|
@@ -9776,8 +9809,8 @@ class SimulatorInterface {
|
|
|
9776
9809
|
}
|
|
9777
9810
|
}
|
|
9778
9811
|
showGeminiLivePanel(simulatorOptions) {
|
|
9779
|
-
if (simulatorOptions.
|
|
9780
|
-
const element = document.createElement(
|
|
9812
|
+
if (simulatorOptions.geminiLivePanel.enabled) {
|
|
9813
|
+
const element = document.createElement(simulatorOptions.geminiLivePanel.element);
|
|
9781
9814
|
document.body.appendChild(element);
|
|
9782
9815
|
this.elements.push(element);
|
|
9783
9816
|
}
|
|
@@ -9821,6 +9854,9 @@ class SimulatorScene extends THREE.Scene {
|
|
|
9821
9854
|
}
|
|
9822
9855
|
async init(simulatorOptions) {
|
|
9823
9856
|
this.addLights();
|
|
9857
|
+
if (simulatorOptions.videoPath) {
|
|
9858
|
+
return;
|
|
9859
|
+
}
|
|
9824
9860
|
if (simulatorOptions.scenePath) {
|
|
9825
9861
|
await this.loadGLTF(simulatorOptions.scenePath, new THREE.Vector3(simulatorOptions.initialScenePosition.x, simulatorOptions.initialScenePosition.y, simulatorOptions.initialScenePosition.z));
|
|
9826
9862
|
}
|
|
@@ -9958,6 +9994,21 @@ class Simulator extends Script {
|
|
|
9958
9994
|
if (this.options.stereo.enabled) {
|
|
9959
9995
|
this.setupStereoCameras(camera);
|
|
9960
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
|
+
}
|
|
9961
10012
|
this.virtualSceneRenderTarget = new THREE.WebGLRenderTarget(renderer.domElement.width, renderer.domElement.height, { stencilBuffer: options.stencil });
|
|
9962
10013
|
const virtualSceneMaterial = new THREE.MeshBasicMaterial({
|
|
9963
10014
|
map: this.virtualSceneRenderTarget.texture,
|
|
@@ -10067,6 +10118,9 @@ class Simulator extends Script {
|
|
|
10067
10118
|
this.sparkRenderer.defaultView.encodeLinear = false;
|
|
10068
10119
|
}
|
|
10069
10120
|
this.renderer.setRenderTarget(null);
|
|
10121
|
+
if (this.backgroundVideoQuad) {
|
|
10122
|
+
this.backgroundVideoQuad.render(this.renderer);
|
|
10123
|
+
}
|
|
10070
10124
|
this.renderer.render(this.simulatorScene, camera);
|
|
10071
10125
|
this.renderer.clearDepth();
|
|
10072
10126
|
}
|
|
@@ -10259,6 +10313,7 @@ class AudioListener extends Script {
|
|
|
10259
10313
|
}
|
|
10260
10314
|
}
|
|
10261
10315
|
|
|
10316
|
+
const DEFAULT_SCHEDULE_AHEAD_TIME = 1.0;
|
|
10262
10317
|
class AudioPlayer extends Script {
|
|
10263
10318
|
constructor(options = {}) {
|
|
10264
10319
|
super();
|
|
@@ -10267,6 +10322,7 @@ class AudioPlayer extends Script {
|
|
|
10267
10322
|
this.nextStartTime = 0;
|
|
10268
10323
|
this.volume = 1.0;
|
|
10269
10324
|
this.category = 'speech';
|
|
10325
|
+
this.scheduleAheadTime = DEFAULT_SCHEDULE_AHEAD_TIME;
|
|
10270
10326
|
this.options = { sampleRate: 24000, channelCount: 1, ...options };
|
|
10271
10327
|
if (options.category) {
|
|
10272
10328
|
this.category = options.category;
|
|
@@ -10331,9 +10387,9 @@ class AudioPlayer extends Script {
|
|
|
10331
10387
|
this.scheduleAudioBuffers();
|
|
10332
10388
|
}
|
|
10333
10389
|
scheduleAudioBuffers() {
|
|
10334
|
-
const SCHEDULE_AHEAD_TIME = 0.2;
|
|
10335
10390
|
while (this.audioQueue.length > 0 &&
|
|
10336
|
-
this.nextStartTime <=
|
|
10391
|
+
this.nextStartTime <=
|
|
10392
|
+
this.audioContext.currentTime + this.scheduleAheadTime) {
|
|
10337
10393
|
const audioBuffer = this.audioQueue.shift();
|
|
10338
10394
|
const currentTime = this.audioContext.currentTime;
|
|
10339
10395
|
const startTime = Math.max(this.nextStartTime, currentTime);
|
|
@@ -14386,6 +14442,216 @@ class XRTransition extends MeshScript {
|
|
|
14386
14442
|
}
|
|
14387
14443
|
}
|
|
14388
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
|
+
|
|
14389
14655
|
/**
|
|
14390
14656
|
* Core is the central engine of the XR Blocks framework, acting as a
|
|
14391
14657
|
* singleton manager for all XR subsystems. Its primary goal is to abstract
|
|
@@ -14445,6 +14711,7 @@ class Core {
|
|
|
14445
14711
|
await script.initPhysics(this.physics);
|
|
14446
14712
|
}
|
|
14447
14713
|
});
|
|
14714
|
+
this.permissionsManager = new PermissionsManager();
|
|
14448
14715
|
if (Core.instance) {
|
|
14449
14716
|
return Core.instance;
|
|
14450
14717
|
}
|
|
@@ -14553,6 +14820,8 @@ class Core {
|
|
|
14553
14820
|
dataFormatPreference: [
|
|
14554
14821
|
this.options.depth.useFloat32 ? 'float32' : 'luminance-alpha',
|
|
14555
14822
|
],
|
|
14823
|
+
depthTypeRequest: options.depth.depthTypeRequest,
|
|
14824
|
+
matchDepthView: options.depth.matchDepthView,
|
|
14556
14825
|
};
|
|
14557
14826
|
this.depth.init(this.camera, options.depth, this.renderer, this.registry, this.scene);
|
|
14558
14827
|
}
|
|
@@ -14589,11 +14858,11 @@ class Core {
|
|
|
14589
14858
|
// Sets up xrButton.
|
|
14590
14859
|
let shouldAutostartSimulator = this.options.xrButton.alwaysAutostartSimulator;
|
|
14591
14860
|
if (!shouldAutostartSimulator && options.xrButton.enabled) {
|
|
14592
|
-
this.xrButton = new XRButton(this.webXRSessionManager, options.xrButton?.startText, options.xrButton?.endText, options.xrButton?.invalidText, options.xrButton?.startSimulatorText, options.xrButton?.
|
|
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);
|
|
14593
14862
|
document.body.appendChild(this.xrButton.domElement);
|
|
14594
14863
|
}
|
|
14595
14864
|
this.webXRSessionManager.addEventListener(WebXRSessionEventType.UNSUPPORTED, () => {
|
|
14596
|
-
if (this.options.
|
|
14865
|
+
if (this.options.enableSimulator) {
|
|
14597
14866
|
this.xrButton?.domElement.remove();
|
|
14598
14867
|
shouldAutostartSimulator = true;
|
|
14599
14868
|
}
|
|
@@ -14716,7 +14985,6 @@ class Core {
|
|
|
14716
14985
|
* scripts.
|
|
14717
14986
|
*/
|
|
14718
14987
|
onXRSessionEnded() {
|
|
14719
|
-
this.startSimulator();
|
|
14720
14988
|
this.scriptsManager.onXRSessionEnded();
|
|
14721
14989
|
}
|
|
14722
14990
|
/**
|