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/xrblocks.js CHANGED
@@ -14,9 +14,9 @@
14
14
  * limitations under the License.
15
15
  *
16
16
  * @file xrblocks.js
17
- * @version v0.3.1
18
- * @commitid 8945d46
19
- * @builddate 2025-11-04T06:32:35.799Z
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 (!frame)
3212
- return;
3213
- this.updateLocalDepth(frame);
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', enableSimulator = 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
+ }) {
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 (enableSimulator) {
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.sessionManager.startSession();
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.geminilive = false;
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
- enableSimulator: true,
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.geminilive) {
9780
- const element = document.createElement('xrblocks-simulator-geminilive');
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 <= this.audioContext.currentTime + SCHEDULE_AHEAD_TIME) {
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?.enableSimulator, 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);
14593
14862
  document.body.appendChild(this.xrButton.domElement);
14594
14863
  }
14595
14864
  this.webXRSessionManager.addEventListener(WebXRSessionEventType.UNSUPPORTED, () => {
14596
- if (this.options.xrButton.enableSimulator) {
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
  /**