xrblocks 0.4.0 → 0.5.1

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.4.0
18
- * @commitid 385db96
19
- * @builddate 2025-11-20T21:08:04.032Z
17
+ * @version v0.5.1
18
+ * @commitid 896d66f
19
+ * @builddate 2025-12-06T06:19:44.629Z
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:
@@ -1849,6 +1849,27 @@ function deepMerge(obj1, obj2) {
1849
1849
  }
1850
1850
  }
1851
1851
 
1852
+ /**
1853
+ * Default parameters for rgb to depth projection.
1854
+ * For RGB and depth, 4:3 and 1:1, respectively.
1855
+ */
1856
+ const DEFAULT_RGB_TO_DEPTH_PARAMS = {
1857
+ scale: 1,
1858
+ scaleX: 0.75,
1859
+ scaleY: 0.63,
1860
+ translateU: 0.2,
1861
+ translateV: -0.02,
1862
+ k1: -0.046,
1863
+ k2: 0,
1864
+ k3: 0,
1865
+ p1: 0,
1866
+ p2: 0,
1867
+ xc: 0,
1868
+ yc: 0,
1869
+ };
1870
+ /**
1871
+ * Configuration options for the device camera.
1872
+ */
1852
1873
  class DeviceCameraOptions {
1853
1874
  constructor(options) {
1854
1875
  this.enabled = false;
@@ -1856,10 +1877,14 @@ class DeviceCameraOptions {
1856
1877
  * Hint for performance optimization on frequent captures.
1857
1878
  */
1858
1879
  this.willCaptureFrequently = false;
1880
+ /**
1881
+ * Parameters for RGB to depth UV mapping given different aspect ratios.
1882
+ */
1883
+ this.rgbToDepthParams = { ...DEFAULT_RGB_TO_DEPTH_PARAMS };
1859
1884
  deepMerge(this, options);
1860
1885
  }
1861
1886
  }
1862
- // Base configuration for all common capture settings
1887
+ // Base configuration for all common capture settings.
1863
1888
  const baseCaptureOptions = {
1864
1889
  enabled: true,
1865
1890
  videoConstraints: {
@@ -2408,6 +2433,8 @@ class DepthOptions {
2408
2433
  // Occlusion pass.
2409
2434
  this.occlusion = { enabled: false };
2410
2435
  this.useFloat32 = true;
2436
+ this.depthTypeRequest = ['raw'];
2437
+ this.matchDepthView = true;
2411
2438
  deepMerge(this, options);
2412
2439
  }
2413
2440
  }
@@ -3314,24 +3341,6 @@ const aspectRatios = {
3314
3341
  depth: 1.0,
3315
3342
  RGB: 4 / 3,
3316
3343
  };
3317
- /**
3318
- * Parameters for RGB to depth UV mapping (manually calibrated for aspect
3319
- * ratios. For RGB and depth, 4:3 and 1:1, respectively.
3320
- */
3321
- const rgbToDepthParams = {
3322
- scale: 1,
3323
- scaleX: 0.75,
3324
- scaleY: 0.63,
3325
- translateU: 0.2,
3326
- translateV: -0.02,
3327
- k1: -0.046,
3328
- k2: 0,
3329
- k3: 0,
3330
- p1: 0,
3331
- p2: 0,
3332
- xc: 0,
3333
- yc: 0,
3334
- };
3335
3344
  /**
3336
3345
  * Maps a UV coordinate from a RGB space to a destination depth space,
3337
3346
  * applying Brown-Conrady distortion and affine transformations based on
@@ -3366,42 +3375,40 @@ function transformRgbToDepthUv(rgbUv, xrDeviceCamera) {
3366
3375
  console.error('Invalid aspect ratios provided.');
3367
3376
  return null;
3368
3377
  }
3369
- // Determine the relative scaling required to fit the overlay within the base
3378
+ const params = xrDeviceCamera?.rgbToDepthParams ?? DEFAULT_RGB_TO_DEPTH_PARAMS;
3379
+ // Determine the relative scaling required to fit the overlay within the base.
3370
3380
  let relativeScaleX, relativeScaleY;
3371
3381
  if (aspectRatios.depth > aspectRatios.RGB) {
3372
- // Base is wider than overlay ("letterboxing")
3382
+ // Base is wider than overlay ("letterboxing").
3373
3383
  relativeScaleY = 1.0;
3374
3384
  relativeScaleX = aspectRatios.RGB / aspectRatios.depth;
3375
3385
  }
3376
3386
  else {
3377
- // Base is narrower than overlay ("pillarboxing")
3387
+ // Base is narrower than overlay ("pillarboxing").
3378
3388
  relativeScaleX = 1.0;
3379
3389
  relativeScaleY = aspectRatios.depth / aspectRatios.RGB;
3380
3390
  }
3381
- // Convert input source UV [0, 1] to a normalized coordinate space [-0.5, 0.5]
3391
+ // Convert input source UV [0, 1] to normalized coordinates in [-0.5, 0.5].
3382
3392
  const u_norm = rgbUv.u - 0.5;
3383
3393
  const v_norm = rgbUv.v - 0.5;
3384
- // Apply the FORWARD Brown-Conrady distortion model
3385
- const u_centered = u_norm - rgbToDepthParams.xc;
3386
- const v_centered = v_norm - rgbToDepthParams.yc;
3394
+ // Apply the FORWARD Brown-Conrady distortion model.
3395
+ const u_centered = u_norm - params.xc;
3396
+ const v_centered = v_norm - params.yc;
3387
3397
  const r2 = u_centered * u_centered + v_centered * v_centered;
3388
- const radial = 1 +
3389
- rgbToDepthParams.k1 * r2 +
3390
- rgbToDepthParams.k2 * r2 * r2 +
3391
- rgbToDepthParams.k3 * r2 * r2 * r2;
3392
- const tanX = 2 * rgbToDepthParams.p1 * u_centered * v_centered +
3393
- rgbToDepthParams.p2 * (r2 + 2 * u_centered * u_centered);
3394
- const tanY = rgbToDepthParams.p1 * (r2 + 2 * v_centered * v_centered) +
3395
- 2 * rgbToDepthParams.p2 * u_centered * v_centered;
3396
- const u_distorted = u_centered * radial + tanX + rgbToDepthParams.xc;
3397
- const v_distorted = v_centered * radial + tanY + rgbToDepthParams.yc;
3398
- // Apply initial aspect ratio scaling and translation
3399
- const u_fitted = u_distorted * relativeScaleX + rgbToDepthParams.translateU;
3400
- const v_fitted = v_distorted * relativeScaleY + rgbToDepthParams.translateV;
3401
- // Apply the final user-controlled scaling (zoom and stretch)
3402
- const finalNormX = u_fitted * rgbToDepthParams.scale * rgbToDepthParams.scaleX;
3403
- const finalNormY = v_fitted * rgbToDepthParams.scale * rgbToDepthParams.scaleY;
3404
- // Convert the final normalized coordinate back to a UV coordinate [0, 1]
3398
+ const radial = 1 + params.k1 * r2 + params.k2 * r2 * r2 + params.k3 * r2 * r2 * r2;
3399
+ const tanX = 2 * params.p1 * u_centered * v_centered +
3400
+ params.p2 * (r2 + 2 * u_centered * u_centered);
3401
+ const tanY = params.p1 * (r2 + 2 * v_centered * v_centered) +
3402
+ 2 * params.p2 * u_centered * v_centered;
3403
+ const u_distorted = u_centered * radial + tanX + params.xc;
3404
+ const v_distorted = v_centered * radial + tanY + params.yc;
3405
+ // Apply initial aspect ratio scaling and translation.
3406
+ const u_fitted = u_distorted * relativeScaleX + params.translateU;
3407
+ const v_fitted = v_distorted * relativeScaleY + params.translateV;
3408
+ // Apply the final user-controlled scaling (zoom and stretch).
3409
+ const finalNormX = u_fitted * params.scale * params.scaleX;
3410
+ const finalNormY = v_fitted * params.scale * params.scaleY;
3411
+ // Convert the final normalized coordinate back to a UV coordinate [0, 1].
3405
3412
  const finalU = finalNormX + 0.5;
3406
3413
  const finalV = finalNormY + 0.5;
3407
3414
  return { u: finalU, v: 1.0 - finalV };
@@ -3665,12 +3672,13 @@ class XRDeviceCamera extends VideoStream {
3665
3672
  /**
3666
3673
  * @param options - The configuration options.
3667
3674
  */
3668
- constructor({ videoConstraints = { facingMode: 'environment' }, willCaptureFrequently = false, } = {}) {
3675
+ constructor({ videoConstraints = { facingMode: 'environment' }, willCaptureFrequently = false, rgbToDepthParams = DEFAULT_RGB_TO_DEPTH_PARAMS, } = {}) {
3669
3676
  super({ willCaptureFrequently });
3670
3677
  this.isInitializing_ = false;
3671
3678
  this.availableDevices_ = [];
3672
3679
  this.currentDeviceIndex_ = -1;
3673
3680
  this.videoConstraints_ = { ...videoConstraints };
3681
+ this.rgbToDepthParams = rgbToDepthParams;
3674
3682
  }
3675
3683
  /**
3676
3684
  * Retrieves the list of available video input devices.
@@ -3722,7 +3730,7 @@ class XRDeviceCamera extends VideoStream {
3722
3730
  return;
3723
3731
  this.isInitializing_ = true;
3724
3732
  this.setState_(StreamState.INITIALIZING);
3725
- // Reset state for the new stream
3733
+ // Reset state for the new stream.
3726
3734
  this.currentTrackSettings_ = undefined;
3727
3735
  this.currentDeviceIndex_ = -1;
3728
3736
  try {
@@ -3756,7 +3764,7 @@ class XRDeviceCamera extends VideoStream {
3756
3764
  if (!videoTracks.length) {
3757
3765
  throw new Error('MediaStream has no video tracks.');
3758
3766
  }
3759
- // After the stream is active, we can get the ID of the track
3767
+ // After the stream is active, we can get the track ID.
3760
3768
  const activeTrack = videoTracks[0];
3761
3769
  this.currentTrackSettings_ = activeTrack.getSettings();
3762
3770
  console.debug('Active track settings:', this.currentTrackSettings_);
@@ -3766,10 +3774,10 @@ class XRDeviceCamera extends VideoStream {
3766
3774
  else {
3767
3775
  console.warn('Stream started without deviceId as it was unavailable');
3768
3776
  }
3769
- this.stop_(); // Stop any previous stream before starting new one
3777
+ this.stop_(); // Stop any previous stream before starting new one.
3770
3778
  this.stream_ = stream;
3771
3779
  this.video_.srcObject = stream;
3772
- this.video_.src = ''; // Required for some browsers to reset the src
3780
+ this.video_.src = ''; // Required for some browsers to reset the src.
3773
3781
  await new Promise((resolve, reject) => {
3774
3782
  this.video_.onloadedmetadata = () => {
3775
3783
  this.handleVideoStreamLoadedMetadata(resolve, reject, true);
@@ -3781,7 +3789,7 @@ class XRDeviceCamera extends VideoStream {
3781
3789
  };
3782
3790
  this.video_.play();
3783
3791
  });
3784
- // Once the stream is loaded and dimensions are known, set the final state
3792
+ // Once stream is loaded and dimensions are known, set the final state.
3785
3793
  const details = {
3786
3794
  width: this.width,
3787
3795
  height: this.height,
@@ -3803,7 +3811,7 @@ class XRDeviceCamera extends VideoStream {
3803
3811
  /**
3804
3812
  * Sets the active camera by its device ID. Removes potentially conflicting
3805
3813
  * constraints such as facingMode.
3806
- * @param deviceId - Device id.
3814
+ * @param deviceId - Device ID
3807
3815
  */
3808
3816
  async setDeviceId(deviceId) {
3809
3817
  const newIndex = this.availableDevices_.findIndex((device) => device.deviceId === deviceId);
@@ -4402,13 +4410,19 @@ class WebXRSessionManager extends THREE.EventDispatcher {
4402
4410
  const XRBUTTON_WRAPPER_ID = 'XRButtonWrapper';
4403
4411
  const XRBUTTON_CLASS = 'XRButton';
4404
4412
  class XRButton {
4405
- constructor(sessionManager, startText = 'ENTER XR', endText = 'END XR', invalidText = 'XR NOT SUPPORTED', startSimulatorText = 'START SIMULATOR', showEnterSimulatorButton = false, startSimulator = () => { }) {
4413
+ constructor(sessionManager, permissionsManager, startText = 'ENTER XR', endText = 'END XR', invalidText = 'XR NOT SUPPORTED', startSimulatorText = 'START SIMULATOR', showEnterSimulatorButton = false, startSimulator = () => { }, permissions = {
4414
+ geolocation: false,
4415
+ camera: false,
4416
+ microphone: false,
4417
+ }) {
4406
4418
  this.sessionManager = sessionManager;
4419
+ this.permissionsManager = permissionsManager;
4407
4420
  this.startText = startText;
4408
4421
  this.endText = endText;
4409
4422
  this.invalidText = invalidText;
4410
4423
  this.startSimulatorText = startSimulatorText;
4411
4424
  this.startSimulator = startSimulator;
4425
+ this.permissions = permissions;
4412
4426
  this.domElement = document.createElement('div');
4413
4427
  this.simulatorButtonElement = document.createElement('button');
4414
4428
  this.xrButtonElement = document.createElement('button');
@@ -4443,7 +4457,17 @@ class XRButton {
4443
4457
  button.innerHTML = this.startText;
4444
4458
  button.disabled = false;
4445
4459
  button.onclick = () => {
4446
- this.sessionManager.startSession();
4460
+ this.permissionsManager
4461
+ .checkAndRequestPermissions(this.permissions)
4462
+ .then((result) => {
4463
+ if (result.granted) {
4464
+ this.sessionManager.startSession();
4465
+ }
4466
+ else {
4467
+ this.xrButtonElement.textContent =
4468
+ 'Error:' + result.error + '\nPlease try again.';
4469
+ }
4470
+ });
4447
4471
  };
4448
4472
  }
4449
4473
  showXRNotSupported() {
@@ -7172,6 +7196,7 @@ class SimulatorOptions {
7172
7196
  constructor(options) {
7173
7197
  this.initialCameraPosition = { x: 0, y: 1.5, z: 0 };
7174
7198
  this.scenePath = XR_BLOCKS_ASSETS_PATH + 'simulator/scenes/XREmulatorsceneV5_livingRoom.glb';
7199
+ this.videoPath = undefined;
7175
7200
  this.initialScenePosition = { x: -1.6, y: 0.3, z: 0 };
7176
7201
  this.defaultMode = SimulatorMode.USER;
7177
7202
  this.defaultHand = Handedness.LEFT;
@@ -7188,7 +7213,10 @@ class SimulatorOptions {
7188
7213
  enabled: true,
7189
7214
  element: 'xrblocks-simulator-hand-pose-panel',
7190
7215
  };
7191
- this.geminilive = false;
7216
+ this.geminiLivePanel = {
7217
+ enabled: false,
7218
+ element: 'xrblocks-simulator-geminilive',
7219
+ };
7192
7220
  this.stereo = {
7193
7221
  enabled: false,
7194
7222
  };
@@ -7436,6 +7464,14 @@ class Options {
7436
7464
  // Whether to autostart the simulator even if WebXR is available.
7437
7465
  alwaysAutostartSimulator: false,
7438
7466
  };
7467
+ /**
7468
+ * Which permissions to request before entering the XR session.
7469
+ */
7470
+ this.permissions = {
7471
+ geolocation: false,
7472
+ camera: false,
7473
+ microphone: false,
7474
+ };
7439
7475
  deepMerge(this, options);
7440
7476
  }
7441
7477
  /**
@@ -7476,6 +7512,7 @@ class Options {
7476
7512
  * @returns The instance for chaining.
7477
7513
  */
7478
7514
  enableObjectDetection() {
7515
+ this.permissions.camera = true;
7479
7516
  this.world.enableObjectDetection();
7480
7517
  return this;
7481
7518
  }
@@ -7486,6 +7523,7 @@ class Options {
7486
7523
  * @returns The instance for chaining.
7487
7524
  */
7488
7525
  enableCamera(facingMode = 'environment') {
7526
+ this.permissions.camera = true;
7489
7527
  this.deviceCamera = new DeviceCameraOptions(facingMode === 'environment'
7490
7528
  ? xrDeviceCameraEnvironmentOptions
7491
7529
  : xrDeviceCameraUserOptions);
@@ -7516,14 +7554,6 @@ class Options {
7516
7554
  this.controllers.visualizeRays = true;
7517
7555
  return this;
7518
7556
  }
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
7557
  /**
7528
7558
  * Enables a standard set of AI features, including Gemini Live.
7529
7559
  * @returns The instance for chaining.
@@ -9785,8 +9815,8 @@ class SimulatorInterface {
9785
9815
  }
9786
9816
  }
9787
9817
  showGeminiLivePanel(simulatorOptions) {
9788
- if (simulatorOptions.geminilive) {
9789
- const element = document.createElement('xrblocks-simulator-geminilive');
9818
+ if (simulatorOptions.geminiLivePanel.enabled) {
9819
+ const element = document.createElement(simulatorOptions.geminiLivePanel.element);
9790
9820
  document.body.appendChild(element);
9791
9821
  this.elements.push(element);
9792
9822
  }
@@ -9830,6 +9860,9 @@ class SimulatorScene extends THREE.Scene {
9830
9860
  }
9831
9861
  async init(simulatorOptions) {
9832
9862
  this.addLights();
9863
+ if (simulatorOptions.videoPath) {
9864
+ return;
9865
+ }
9833
9866
  if (simulatorOptions.scenePath) {
9834
9867
  await this.loadGLTF(simulatorOptions.scenePath, new THREE.Vector3(simulatorOptions.initialScenePosition.x, simulatorOptions.initialScenePosition.y, simulatorOptions.initialScenePosition.z));
9835
9868
  }
@@ -9967,6 +10000,21 @@ class Simulator extends Script {
9967
10000
  if (this.options.stereo.enabled) {
9968
10001
  this.setupStereoCameras(camera);
9969
10002
  }
10003
+ if (this.options.videoPath) {
10004
+ this.videoElement = document.createElement('video');
10005
+ this.videoElement.src = this.options.videoPath;
10006
+ this.videoElement.loop = true;
10007
+ this.videoElement.muted = true;
10008
+ this.videoElement.play().catch((e) => {
10009
+ console.error(`Simulator: Failed to play video at ${this.options.videoPath}`, e);
10010
+ });
10011
+ this.videoElement.addEventListener('error', () => {
10012
+ console.error(`Simulator: Error loading video at ${this.options.videoPath}`, this.videoElement?.error);
10013
+ });
10014
+ const videoTexture = new THREE.VideoTexture(this.videoElement);
10015
+ videoTexture.colorSpace = THREE.SRGBColorSpace;
10016
+ this.backgroundVideoQuad = new FullScreenQuad(new THREE.MeshBasicMaterial({ map: videoTexture }));
10017
+ }
9970
10018
  this.virtualSceneRenderTarget = new THREE.WebGLRenderTarget(renderer.domElement.width, renderer.domElement.height, { stencilBuffer: options.stencil });
9971
10019
  const virtualSceneMaterial = new THREE.MeshBasicMaterial({
9972
10020
  map: this.virtualSceneRenderTarget.texture,
@@ -10076,6 +10124,9 @@ class Simulator extends Script {
10076
10124
  this.sparkRenderer.defaultView.encodeLinear = false;
10077
10125
  }
10078
10126
  this.renderer.setRenderTarget(null);
10127
+ if (this.backgroundVideoQuad) {
10128
+ this.backgroundVideoQuad.render(this.renderer);
10129
+ }
10079
10130
  this.renderer.render(this.simulatorScene, camera);
10080
10131
  this.renderer.clearDepth();
10081
10132
  }
@@ -11751,7 +11802,7 @@ class TextView extends View {
11751
11802
  }
11752
11803
  setTextColor(color) {
11753
11804
  if (Text && this.textObj instanceof Text) {
11754
- this.textObj.color = color;
11805
+ this.textObj.color = getColorHex(color);
11755
11806
  }
11756
11807
  }
11757
11808
  /**
@@ -12141,7 +12192,7 @@ class TextButton extends TextView {
12141
12192
  */
12142
12193
  constructor(options = {}) {
12143
12194
  const geometry = new THREE.PlaneGeometry(1, 1);
12144
- const colorVec4 = getVec4ByColorString(options.backgroundColor ?? '#00000000');
12195
+ const colorVec4 = getVec4ByColorString(options.backgroundColor ?? '#000000');
12145
12196
  const { opacity = 0.0, radius = SquircleShader.uniforms.uRadius.value, boxSize = SquircleShader.uniforms.uBoxSize.value, } = options;
12146
12197
  const uniforms = {
12147
12198
  ...SquircleShader.uniforms,
@@ -12191,6 +12242,9 @@ class TextButton extends TextView {
12191
12242
  // Applies our own overrides to the default values.
12192
12243
  this.fontSize = options.fontSize ?? this.fontSize;
12193
12244
  this.fontColor = options.fontColor ?? this.fontColor;
12245
+ this.hoverColor = options.hoverColor ?? this.hoverColor;
12246
+ this.selectedFontColor =
12247
+ options.selectedFontColor ?? this.selectedFontColor;
12194
12248
  this.width = options.width ?? this.width;
12195
12249
  this.height = options.height ?? this.height;
12196
12250
  }
@@ -12213,20 +12267,19 @@ class TextButton extends TextView {
12213
12267
  if (!this.textObj) {
12214
12268
  return;
12215
12269
  }
12216
- if (this.textObj) {
12217
- this.textObj.renderOrder = this.renderOrder + 1;
12218
- }
12270
+ // Update render order to ensure text appears on top of the button mesh
12271
+ this.textObj.renderOrder = this.renderOrder + 1;
12219
12272
  const ux = this.ux;
12220
12273
  if (ux.isHovered()) {
12221
12274
  if (ux.isSelected()) {
12222
- this.setTextColor(0x666666);
12275
+ this.setTextColor(this.selectedFontColor);
12223
12276
  }
12224
12277
  else {
12225
- this.setTextColor(0xaaaaaa);
12278
+ this.setTextColor(this.hoverColor);
12226
12279
  }
12227
12280
  }
12228
12281
  else {
12229
- this.setTextColor(0xffffff);
12282
+ this.setTextColor(this.fontColor);
12230
12283
  this.uniforms.uOpacity.value = this.defaultOpacity * this.opacity;
12231
12284
  }
12232
12285
  }
@@ -13842,6 +13895,7 @@ class ObjectDetector extends Script {
13842
13895
  }
13843
13896
  if (this.options.objects.showDebugVisualizations) {
13844
13897
  this._visualizeBoundingBoxesOnImage(base64Image, parsedResponse);
13898
+ this._visualizeDepthMap(cachedDepthArray);
13845
13899
  }
13846
13900
  const detectionPromises = parsedResponse.map(async (item) => {
13847
13901
  const { ymin, xmin, ymax, xmax, objectName, ...additionalData } = item || {};
@@ -13890,7 +13944,7 @@ class ObjectDetector extends Script {
13890
13944
  * Retrieves a list of currently detected objects.
13891
13945
  *
13892
13946
  * @param label - The semantic label to filter by (e.g., 'chair'). If null,
13893
- * all objects are returned.
13947
+ * all objects are returned.
13894
13948
  * @returns An array of `Object` instances.
13895
13949
  */
13896
13950
  get(label = null) {
@@ -13927,8 +13981,7 @@ class ObjectDetector extends Script {
13927
13981
  * Draws the detected bounding boxes on the input image and triggers a
13928
13982
  * download for debugging.
13929
13983
  * @param base64Image - The base64 encoded input image.
13930
- * @param detections - The array of detected objects from the
13931
- * AI response.
13984
+ * @param detections - The array of detected objects from the AI response.
13932
13985
  */
13933
13986
  _visualizeBoundingBoxesOnImage(base64Image, detections) {
13934
13987
  const img = new Image();
@@ -13977,6 +14030,71 @@ class ObjectDetector extends Script {
13977
14030
  };
13978
14031
  img.src = base64Image;
13979
14032
  }
14033
+ /**
14034
+ * Generates a visual representation of the depth map, normalized to 0-1 range,
14035
+ * and triggers a download for debugging.
14036
+ * @param depthArray - The raw depth data array.
14037
+ */
14038
+ _visualizeDepthMap(depthArray) {
14039
+ const width = this.depth.width;
14040
+ const height = this.depth.height;
14041
+ if (!width || !height || depthArray.length === 0) {
14042
+ console.warn('Cannot visualize depth map: missing dimensions or data.');
14043
+ return;
14044
+ }
14045
+ // 1. Find Min/Max for normalization (ignoring 0/invalid depth).
14046
+ let min = Infinity;
14047
+ let max = -Infinity;
14048
+ for (let i = 0; i < depthArray.length; ++i) {
14049
+ const val = depthArray[i];
14050
+ if (val > 0) {
14051
+ if (val < min)
14052
+ min = val;
14053
+ if (val > max)
14054
+ max = val;
14055
+ }
14056
+ }
14057
+ // Handle edge case where no valid depth exists.
14058
+ if (min === Infinity) {
14059
+ min = 0;
14060
+ max = 1;
14061
+ }
14062
+ if (min === max) {
14063
+ max = min + 1; // Avoid divide by zero
14064
+ }
14065
+ // 2. Create Canvas.
14066
+ const canvas = document.createElement('canvas');
14067
+ canvas.width = width;
14068
+ canvas.height = height;
14069
+ const ctx = canvas.getContext('2d');
14070
+ const imageData = ctx.createImageData(width, height);
14071
+ const data = imageData.data;
14072
+ // 3. Fill Pixels.
14073
+ for (let i = 0; i < depthArray.length; ++i) {
14074
+ const raw = depthArray[i];
14075
+ // Normalize to 0-1.
14076
+ // Typically 0 means invalid/sky in some depth APIs, so we keep it black.
14077
+ // Otherwise, map [min, max] to [0, 1].
14078
+ const normalized = raw === 0 ? 0 : (raw - min) / (max - min);
14079
+ const byteVal = Math.floor(normalized * 255);
14080
+ const stride = i * 4;
14081
+ data[stride] = byteVal; // R
14082
+ data[stride + 1] = byteVal; // G
14083
+ data[stride + 2] = byteVal; // B
14084
+ data[stride + 3] = 255; // Alpha
14085
+ }
14086
+ ctx.putImageData(imageData, 0, 0);
14087
+ // 4. Download.
14088
+ const timestamp = new Date()
14089
+ .toISOString()
14090
+ .slice(0, 19)
14091
+ .replace('T', '_')
14092
+ .replace(/:/g, '-');
14093
+ const link = document.createElement('a');
14094
+ link.download = `depth_debug_${timestamp}.png`;
14095
+ link.href = canvas.toDataURL('image/png');
14096
+ link.click();
14097
+ }
13980
14098
  /**
13981
14099
  * Creates a simple debug visualization for an object based on its position
13982
14100
  * (center of its 2D detection bounding box).
@@ -14397,6 +14515,216 @@ class XRTransition extends MeshScript {
14397
14515
  }
14398
14516
  }
14399
14517
 
14518
+ /**
14519
+ * A utility class to manage and request browser permissions for
14520
+ * Location, Camera, and Microphone.
14521
+ */
14522
+ class PermissionsManager {
14523
+ /**
14524
+ * Requests permission to access the user's geolocation.
14525
+ * Note: This actually attempts to fetch the position to trigger the prompt.
14526
+ */
14527
+ async requestLocationPermission() {
14528
+ if (!('geolocation' in navigator)) {
14529
+ return {
14530
+ granted: false,
14531
+ status: 'error',
14532
+ error: 'Geolocation is not supported by this browser.',
14533
+ };
14534
+ }
14535
+ return new Promise((resolve) => {
14536
+ navigator.geolocation.getCurrentPosition(() => {
14537
+ resolve({ granted: true, status: 'granted' });
14538
+ }, (error) => {
14539
+ let errorMsg = 'Unknown error';
14540
+ switch (error.code) {
14541
+ case error.PERMISSION_DENIED:
14542
+ errorMsg = 'User denied the request.';
14543
+ break;
14544
+ case error.POSITION_UNAVAILABLE:
14545
+ errorMsg = 'Location information is unavailable.';
14546
+ break;
14547
+ case error.TIMEOUT:
14548
+ errorMsg = 'The request to get user location timed out.';
14549
+ break;
14550
+ }
14551
+ resolve({ granted: false, status: 'denied', error: errorMsg });
14552
+ }, { timeout: 10000 } // 10 second timeout
14553
+ );
14554
+ });
14555
+ }
14556
+ /**
14557
+ * Requests permission to access the microphone.
14558
+ * Opens a stream to trigger the prompt, then immediately closes it.
14559
+ */
14560
+ async requestMicrophonePermission() {
14561
+ return this.requestMediaPermission({ audio: true });
14562
+ }
14563
+ /**
14564
+ * Requests permission to access the camera.
14565
+ * Opens a stream to trigger the prompt, then immediately closes it.
14566
+ */
14567
+ async requestCameraPermission() {
14568
+ return this.requestMediaPermission({ video: true });
14569
+ }
14570
+ /**
14571
+ * Requests permission for both camera and microphone simultaneously.
14572
+ */
14573
+ async requestAVPermission() {
14574
+ return this.requestMediaPermission({ video: true, audio: true });
14575
+ }
14576
+ /**
14577
+ * Internal helper to handle getUserMedia requests.
14578
+ * Crucially, this stops the tracks immediately after permission is granted
14579
+ * so the hardware doesn't remain active.
14580
+ */
14581
+ async requestMediaPermission(constraints) {
14582
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
14583
+ return {
14584
+ granted: false,
14585
+ status: 'error',
14586
+ error: 'Media Devices API is not supported by this browser.',
14587
+ };
14588
+ }
14589
+ try {
14590
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
14591
+ // Permission granted. Now stop the stream to release hardware.
14592
+ stream.getTracks().forEach((track) => track.stop());
14593
+ return { granted: true, status: 'granted' };
14594
+ }
14595
+ catch (err) {
14596
+ // Handle common getUserMedia errors
14597
+ const status = 'denied';
14598
+ let errorMessage = 'Permission denied';
14599
+ if (err instanceof Error) {
14600
+ if (err.name === 'NotFoundError' ||
14601
+ err.name === 'DevicesNotFoundError') {
14602
+ return {
14603
+ granted: false,
14604
+ status: 'error',
14605
+ error: 'Hardware not found.',
14606
+ };
14607
+ }
14608
+ errorMessage = err.message || errorMessage;
14609
+ }
14610
+ return { granted: false, status: status, error: errorMessage };
14611
+ }
14612
+ }
14613
+ /**
14614
+ * Requests multiple permissions sequentially.
14615
+ * Returns a single result: granted is true only if ALL requested permissions are granted.
14616
+ */
14617
+ async checkAndRequestPermissions({ geolocation = false, camera = false, microphone = false, }) {
14618
+ const results = [];
14619
+ // 1. Handle Location
14620
+ if (geolocation) {
14621
+ const status = await this.checkPermissionStatus('geolocation');
14622
+ if (status === 'granted') {
14623
+ results.push({ granted: true, status: 'granted' });
14624
+ }
14625
+ else {
14626
+ results.push(await this.requestLocationPermission());
14627
+ }
14628
+ }
14629
+ // 2. Handle Media (Camera & Mic)
14630
+ // We group these because requestAVPermission can ask for both in one prompt
14631
+ if (camera && microphone) {
14632
+ const camStatus = await this.checkPermissionStatus('camera');
14633
+ const micStatus = await this.checkPermissionStatus('microphone');
14634
+ if (camStatus === 'granted' && micStatus === 'granted') {
14635
+ results.push({ granted: true, status: 'granted' });
14636
+ }
14637
+ else if (camStatus === 'granted') {
14638
+ // Only need mic
14639
+ results.push(await this.requestMicrophonePermission());
14640
+ }
14641
+ else if (micStatus === 'granted') {
14642
+ // Only need camera
14643
+ results.push(await this.requestCameraPermission());
14644
+ }
14645
+ else {
14646
+ // Need both
14647
+ results.push(await this.requestAVPermission());
14648
+ }
14649
+ }
14650
+ else if (camera) {
14651
+ const status = await this.checkPermissionStatus('camera');
14652
+ if (status === 'granted') {
14653
+ results.push({ granted: true, status: 'granted' });
14654
+ }
14655
+ else {
14656
+ results.push(await this.requestCameraPermission());
14657
+ }
14658
+ }
14659
+ else if (microphone) {
14660
+ const status = await this.checkPermissionStatus('microphone');
14661
+ if (status === 'granted') {
14662
+ results.push({ granted: true, status: 'granted' });
14663
+ }
14664
+ else {
14665
+ results.push(await this.requestMicrophonePermission());
14666
+ }
14667
+ }
14668
+ // 3. Aggregate results
14669
+ if (results.length === 0) {
14670
+ return { granted: true, status: 'granted' };
14671
+ }
14672
+ const allGranted = results.every((r) => r.granted);
14673
+ const anyDenied = results.find((r) => r.status === 'denied');
14674
+ const anyError = results.find((r) => r.status === 'error');
14675
+ // Aggregate errors
14676
+ const errors = results
14677
+ .filter((r) => r.error)
14678
+ .map((r) => r.error)
14679
+ .join(' | ');
14680
+ let finalStatus = 'granted';
14681
+ if (anyError)
14682
+ finalStatus = 'error';
14683
+ else if (anyDenied)
14684
+ finalStatus = 'denied';
14685
+ return {
14686
+ granted: allGranted,
14687
+ status: finalStatus,
14688
+ error: errors || undefined,
14689
+ };
14690
+ }
14691
+ /**
14692
+ * Checks the current status of a permission without triggering a prompt.
14693
+ * Useful for UI state (e.g., disabling buttons if already denied).
14694
+ * * @param permissionName - 'geolocation', 'camera', or 'microphone'
14695
+ */
14696
+ async checkPermissionStatus(permissionName) {
14697
+ if (!navigator.permissions || !navigator.permissions.query) {
14698
+ return 'unknown';
14699
+ }
14700
+ try {
14701
+ let queryName;
14702
+ // Map friendly names to API PermissionName types
14703
+ // Note: 'camera' and 'microphone' are part of the newer spec,
14704
+ // but strictly Typed TypeScript might expect specific descriptor objects.
14705
+ if (permissionName === 'geolocation') {
14706
+ queryName = 'geolocation';
14707
+ }
14708
+ else if (permissionName === 'camera' ||
14709
+ permissionName === 'microphone') {
14710
+ const descriptor = { name: permissionName };
14711
+ const result = await navigator.permissions.query(descriptor);
14712
+ return result.state;
14713
+ }
14714
+ else {
14715
+ return 'unknown';
14716
+ }
14717
+ const result = await navigator.permissions.query({ name: queryName });
14718
+ return result.state;
14719
+ }
14720
+ catch (error) {
14721
+ // Firefox and Safari have incomplete Permissions API support
14722
+ console.warn(`Error checking permission status for ${permissionName}`, error);
14723
+ return 'unknown';
14724
+ }
14725
+ }
14726
+ }
14727
+
14400
14728
  /**
14401
14729
  * Core is the central engine of the XR Blocks framework, acting as a
14402
14730
  * singleton manager for all XR subsystems. Its primary goal is to abstract
@@ -14456,6 +14784,7 @@ class Core {
14456
14784
  await script.initPhysics(this.physics);
14457
14785
  }
14458
14786
  });
14787
+ this.permissionsManager = new PermissionsManager();
14459
14788
  if (Core.instance) {
14460
14789
  return Core.instance;
14461
14790
  }
@@ -14564,6 +14893,8 @@ class Core {
14564
14893
  dataFormatPreference: [
14565
14894
  this.options.depth.useFloat32 ? 'float32' : 'luminance-alpha',
14566
14895
  ],
14896
+ depthTypeRequest: options.depth.depthTypeRequest,
14897
+ matchDepthView: options.depth.matchDepthView,
14567
14898
  };
14568
14899
  this.depth.init(this.camera, options.depth, this.renderer, this.registry, this.scene);
14569
14900
  }
@@ -14600,7 +14931,7 @@ class Core {
14600
14931
  // Sets up xrButton.
14601
14932
  let shouldAutostartSimulator = this.options.xrButton.alwaysAutostartSimulator;
14602
14933
  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));
14934
+ 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
14935
  document.body.appendChild(this.xrButton.domElement);
14605
14936
  }
14606
14937
  this.webXRSessionManager.addEventListener(WebXRSessionEventType.UNSUPPORTED, () => {
@@ -16828,5 +17159,5 @@ class VideoFileStream extends VideoStream {
16828
17159
  }
16829
17160
  }
16830
17161
 
16831
- export { AI, AIOptions, AVERAGE_IPD_METERS, ActiveControllers, Agent, AnimatableNumber, AudioListener, AudioPlayer, BACK, BackgroundMusic, CategoryVolumes, Col, Core, CoreSound, DEFAULT_DEVICE_CAMERA_HEIGHT, DEFAULT_DEVICE_CAMERA_WIDTH, DOWN, Depth, DepthMesh, DepthMeshOptions, DepthOptions, DepthTextures, DetectedObject, DetectedPlane, DeviceCameraOptions, DragManager, DragMode, ExitButton, FORWARD, FreestandingSlider, GazeController, Gemini, GeminiOptions, GenerateSkyboxTool, GestureRecognition, GestureRecognitionOptions, GetWeatherTool, Grid, HAND_BONE_IDX_CONNECTION_MAP, HAND_JOINT_COUNT, HAND_JOINT_IDX_CONNECTION_MAP, HAND_JOINT_NAMES, Handedness, Hands, HandsOptions, HorizontalPager, IconButton, IconView, ImageView, Input, InputOptions, Keycodes, LEFT, LEFT_VIEW_ONLY_LAYER, LabelView, Lighting, LightingOptions, LoadingSpinnerManager, MaterialSymbolsView, MeshScript, ModelLoader, ModelViewer, MouseController, NEXT_SIMULATOR_MODE, NUM_HANDS, OCCLUDABLE_ITEMS_LAYER, ObjectDetector, ObjectsOptions, OcclusionPass, OcclusionUtils, OpenAI, OpenAIOptions, Options, PageIndicator, Pager, PagerState, Panel, PanelMesh, Physics, PhysicsOptions, PinchOnButtonAction, PlaneDetector, PlanesOptions, RIGHT, RIGHT_VIEW_ONLY_LAYER, Registry, Reticle, ReticleOptions, RotationRaycastMesh, Row, SIMULATOR_HAND_POSE_NAMES, SIMULATOR_HAND_POSE_TO_JOINTS_LEFT, SIMULATOR_HAND_POSE_TO_JOINTS_RIGHT, SOUND_PRESETS, ScreenshotSynthesizer, Script, ScriptMixin, ScriptsManager, ScrollingTroikaTextView, SetSimulatorModeEvent, ShowHandsAction, Simulator, SimulatorCamera, SimulatorControlMode, SimulatorControllerState, SimulatorControls, SimulatorDepth, SimulatorDepthMaterial, SimulatorHandPose, SimulatorHandPoseChangeRequestEvent, SimulatorHands, SimulatorInterface, SimulatorMediaDeviceInfo, SimulatorMode, SimulatorOptions, SimulatorRenderMode, SimulatorScene, SimulatorUser, SimulatorUserAction, SketchPanel, SkyboxAgent, SoundOptions, SoundSynthesizer, SpatialAudio, SpatialPanel, SpeechRecognizer, SpeechRecognizerOptions, SpeechSynthesizer, SpeechSynthesizerOptions, SplatAnchor, StreamState, TextButton, TextScrollerState, TextView, Tool, UI, UI_OVERLAY_LAYER, UP, UX, User, VIEW_DEPTH_GAP, VerticalPager, VideoFileStream, VideoStream, VideoView, View, VolumeCategory, WaitFrame, WalkTowardsPanelAction, World, WorldOptions, XRButton, XRDeviceCamera, XREffects, XRPass, XRTransitionOptions, XR_BLOCKS_ASSETS_PATH, ZERO_VECTOR3, add, ai, aspectRatios, callInitWithDependencyInjection, clamp, clampRotationToAngle, core, cropImage, extractYaw, getColorHex, getDeltaTime, getUrlParamBool, getUrlParamFloat, getUrlParamInt, getUrlParameter, getVec4ByColorString, getXrCameraLeft, getXrCameraRight, init, initScript, lerp, loadStereoImageAsTextures, loadingSpinnerManager, lookAtRotation, objectIsDescendantOf, parseBase64DataURL, placeObjectAtIntersectionFacingTarget, print, rgbToDepthParams, scene, showOnlyInLeftEye, showOnlyInRightEye, showReticleOnDepthMesh, transformRgbToDepthUv, transformRgbUvToWorld, traverseUtil, uninitScript, urlParams, user, world, xrDepthMeshOptions, xrDepthMeshPhysicsOptions, xrDepthMeshVisualizationOptions, xrDeviceCameraEnvironmentContinuousOptions, xrDeviceCameraEnvironmentOptions, xrDeviceCameraUserContinuousOptions, xrDeviceCameraUserOptions };
17162
+ export { AI, AIOptions, AVERAGE_IPD_METERS, ActiveControllers, Agent, AnimatableNumber, AudioListener, AudioPlayer, BACK, BackgroundMusic, CategoryVolumes, Col, Core, CoreSound, DEFAULT_DEVICE_CAMERA_HEIGHT, DEFAULT_DEVICE_CAMERA_WIDTH, DEFAULT_RGB_TO_DEPTH_PARAMS, DOWN, Depth, DepthMesh, DepthMeshOptions, DepthOptions, DepthTextures, DetectedObject, DetectedPlane, DeviceCameraOptions, DragManager, DragMode, ExitButton, FORWARD, FreestandingSlider, GazeController, Gemini, GeminiOptions, GenerateSkyboxTool, GestureRecognition, GestureRecognitionOptions, GetWeatherTool, Grid, HAND_BONE_IDX_CONNECTION_MAP, HAND_JOINT_COUNT, HAND_JOINT_IDX_CONNECTION_MAP, HAND_JOINT_NAMES, Handedness, Hands, HandsOptions, HorizontalPager, IconButton, IconView, ImageView, Input, InputOptions, Keycodes, LEFT, LEFT_VIEW_ONLY_LAYER, LabelView, Lighting, LightingOptions, LoadingSpinnerManager, MaterialSymbolsView, MeshScript, ModelLoader, ModelViewer, MouseController, NEXT_SIMULATOR_MODE, NUM_HANDS, OCCLUDABLE_ITEMS_LAYER, ObjectDetector, ObjectsOptions, OcclusionPass, OcclusionUtils, OpenAI, OpenAIOptions, Options, PageIndicator, Pager, PagerState, Panel, PanelMesh, Physics, PhysicsOptions, PinchOnButtonAction, PlaneDetector, PlanesOptions, RIGHT, RIGHT_VIEW_ONLY_LAYER, Registry, Reticle, ReticleOptions, RotationRaycastMesh, Row, SIMULATOR_HAND_POSE_NAMES, SIMULATOR_HAND_POSE_TO_JOINTS_LEFT, SIMULATOR_HAND_POSE_TO_JOINTS_RIGHT, SOUND_PRESETS, ScreenshotSynthesizer, Script, ScriptMixin, ScriptsManager, ScrollingTroikaTextView, SetSimulatorModeEvent, ShowHandsAction, Simulator, SimulatorCamera, SimulatorControlMode, SimulatorControllerState, SimulatorControls, SimulatorDepth, SimulatorDepthMaterial, SimulatorHandPose, SimulatorHandPoseChangeRequestEvent, SimulatorHands, SimulatorInterface, SimulatorMediaDeviceInfo, SimulatorMode, SimulatorOptions, SimulatorRenderMode, SimulatorScene, SimulatorUser, SimulatorUserAction, SketchPanel, SkyboxAgent, SoundOptions, SoundSynthesizer, SpatialAudio, SpatialPanel, SpeechRecognizer, SpeechRecognizerOptions, SpeechSynthesizer, SpeechSynthesizerOptions, SplatAnchor, StreamState, TextButton, TextScrollerState, TextView, Tool, UI, UI_OVERLAY_LAYER, UP, UX, User, VIEW_DEPTH_GAP, VerticalPager, VideoFileStream, VideoStream, VideoView, View, VolumeCategory, WaitFrame, WalkTowardsPanelAction, World, WorldOptions, XRButton, XRDeviceCamera, XREffects, XRPass, XRTransitionOptions, XR_BLOCKS_ASSETS_PATH, ZERO_VECTOR3, add, ai, aspectRatios, callInitWithDependencyInjection, clamp, clampRotationToAngle, core, cropImage, extractYaw, getColorHex, getDeltaTime, getUrlParamBool, getUrlParamFloat, getUrlParamInt, getUrlParameter, getVec4ByColorString, getXrCameraLeft, getXrCameraRight, init, initScript, lerp, loadStereoImageAsTextures, loadingSpinnerManager, lookAtRotation, objectIsDescendantOf, parseBase64DataURL, placeObjectAtIntersectionFacingTarget, print, scene, showOnlyInLeftEye, showOnlyInRightEye, showReticleOnDepthMesh, transformRgbToDepthUv, transformRgbUvToWorld, traverseUtil, uninitScript, urlParams, user, world, xrDepthMeshOptions, xrDepthMeshPhysicsOptions, xrDepthMeshVisualizationOptions, xrDeviceCameraEnvironmentContinuousOptions, xrDeviceCameraEnvironmentOptions, xrDeviceCameraUserContinuousOptions, xrDeviceCameraUserOptions };
16832
17163
  //# sourceMappingURL=xrblocks.js.map