iwer 2.1.1 → 2.2.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.
Files changed (59) hide show
  1. package/build/iwer.js +1760 -5
  2. package/build/iwer.min.js +12 -12
  3. package/build/iwer.module.js +1751 -6
  4. package/build/iwer.module.min.js +12 -12
  5. package/lib/depth/XRDepthInformation.d.ts +38 -0
  6. package/lib/depth/XRDepthInformation.d.ts.map +1 -0
  7. package/lib/depth/XRDepthInformation.js +55 -0
  8. package/lib/depth/XRDepthInformation.js.map +1 -0
  9. package/lib/device/XRController.d.ts +20 -0
  10. package/lib/device/XRController.d.ts.map +1 -1
  11. package/lib/device/XRController.js +56 -0
  12. package/lib/device/XRController.js.map +1 -1
  13. package/lib/device/XRDevice.d.ts +45 -1
  14. package/lib/device/XRDevice.d.ts.map +1 -1
  15. package/lib/device/XRDevice.js +88 -0
  16. package/lib/device/XRDevice.js.map +1 -1
  17. package/lib/frameloop/XRFrame.d.ts +4 -0
  18. package/lib/frameloop/XRFrame.d.ts.map +1 -1
  19. package/lib/frameloop/XRFrame.js +12 -1
  20. package/lib/frameloop/XRFrame.js.map +1 -1
  21. package/lib/index.d.ts +5 -0
  22. package/lib/index.d.ts.map +1 -1
  23. package/lib/index.js +6 -0
  24. package/lib/index.js.map +1 -1
  25. package/lib/private.d.ts +1 -0
  26. package/lib/private.d.ts.map +1 -1
  27. package/lib/private.js +1 -0
  28. package/lib/private.js.map +1 -1
  29. package/lib/remote/RemoteControlInterface.d.ts +172 -0
  30. package/lib/remote/RemoteControlInterface.d.ts.map +1 -0
  31. package/lib/remote/RemoteControlInterface.js +1240 -0
  32. package/lib/remote/RemoteControlInterface.js.map +1 -0
  33. package/lib/remote/index.d.ts +9 -0
  34. package/lib/remote/index.d.ts.map +1 -0
  35. package/lib/remote/index.js +8 -0
  36. package/lib/remote/index.js.map +1 -0
  37. package/lib/remote/types.d.ts +348 -0
  38. package/lib/remote/types.d.ts.map +1 -0
  39. package/lib/remote/types.js +8 -0
  40. package/lib/remote/types.js.map +1 -0
  41. package/lib/session/XRSession.d.ts +7 -0
  42. package/lib/session/XRSession.d.ts.map +1 -1
  43. package/lib/session/XRSession.js +42 -0
  44. package/lib/session/XRSession.js.map +1 -1
  45. package/lib/types/state.d.ts +46 -0
  46. package/lib/types/state.d.ts.map +1 -0
  47. package/lib/types/state.js +8 -0
  48. package/lib/types/state.js.map +1 -0
  49. package/lib/utils/control-math.d.ts +64 -0
  50. package/lib/utils/control-math.d.ts.map +1 -0
  51. package/lib/utils/control-math.js +238 -0
  52. package/lib/utils/control-math.js.map +1 -0
  53. package/lib/version.d.ts +1 -1
  54. package/lib/version.js +1 -1
  55. package/package.json +10 -5
  56. package/lib/layers/XRWebGLBinding.d.ts +0 -92
  57. package/lib/layers/XRWebGLBinding.d.ts.map +0 -1
  58. package/lib/layers/XRWebGLBinding.js +0 -186
  59. package/lib/layers/XRWebGLBinding.js.map +0 -1
package/build/iwer.js CHANGED
@@ -764,7 +764,7 @@
764
764
  * @param {Number} t interpolation amount, in the range [0-1], between the two inputs
765
765
  * @returns {vec3} out
766
766
  */
767
- function lerp(out, a, b, t) {
767
+ function lerp$1(out, a, b, t) {
768
768
  var ax = a[0];
769
769
  var ay = a[1];
770
770
  var az = a[2];
@@ -774,6 +774,27 @@
774
774
  return out;
775
775
  }
776
776
 
777
+ /**
778
+ * Transforms the vec3 with a mat4.
779
+ * 4th vector component is implicitly '1'
780
+ *
781
+ * @param {vec3} out the receiving vector
782
+ * @param {ReadonlyVec3} a the vector to transform
783
+ * @param {ReadonlyMat4} m matrix to transform with
784
+ * @returns {vec3} out
785
+ */
786
+ function transformMat4$1(out, a, m) {
787
+ var x = a[0],
788
+ y = a[1],
789
+ z = a[2];
790
+ var w = m[3] * x + m[7] * y + m[11] * z + m[15];
791
+ w = w || 1.0;
792
+ out[0] = (m[0] * x + m[4] * y + m[8] * z + m[12]) / w;
793
+ out[1] = (m[1] * x + m[5] * y + m[9] * z + m[13]) / w;
794
+ out[2] = (m[2] * x + m[6] * y + m[10] * z + m[14]) / w;
795
+ return out;
796
+ }
797
+
777
798
  /**
778
799
  * Transforms the vec3 with a quat
779
800
  * Can also be used for dual quaternions. (Multiply it with the real part)
@@ -1354,6 +1375,7 @@
1354
1375
  const P_VIEW = Symbol('@iwer/xr-view');
1355
1376
  const P_VIEWPORT = Symbol('@iwer/xr-viewport');
1356
1377
  const P_RAY = Symbol('@iwer/xr-ray');
1378
+ const P_DEPTH_INFO = Symbol('@iwer/xr-depth-info');
1357
1379
  const P_HIT_TEST = Symbol('@iwer/xr-hit-test');
1358
1380
 
1359
1381
  /**
@@ -1538,6 +1560,1481 @@
1538
1560
  }
1539
1561
  }
1540
1562
 
1563
+ /**
1564
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
1565
+ *
1566
+ * This source code is licensed under the MIT license found in the
1567
+ * LICENSE file in the root directory of this source tree.
1568
+ */
1569
+ /**
1570
+ * Convert a Vector3-like object to a plain Vec3 object
1571
+ */
1572
+ function vec3ToObj(v) {
1573
+ return { x: v.x, y: v.y, z: v.z };
1574
+ }
1575
+ /**
1576
+ * Convert a Quaternion-like object to a plain Quat object
1577
+ */
1578
+ function quatToObj(q) {
1579
+ return { x: q.x, y: q.y, z: q.z, w: q.w };
1580
+ }
1581
+ /**
1582
+ * Convert quaternion to euler angles (in degrees)
1583
+ * Uses YXZ order (yaw-pitch-roll) which is standard for XR:
1584
+ * - Yaw: rotation around Y axis (turning left/right)
1585
+ * - Pitch: rotation around X axis (looking up/down)
1586
+ * - Roll: rotation around Z axis (tilting head)
1587
+ */
1588
+ function quatToEuler(q) {
1589
+ const { x, y, z, w } = q;
1590
+ const RAD_TO_DEG = 180 / Math.PI;
1591
+ // YXZ order
1592
+ const sinp = Math.max(-1, Math.min(1, 2 * (w * x - y * z)));
1593
+ let pitch;
1594
+ if (Math.abs(sinp) >= 1) {
1595
+ pitch = (Math.sign(sinp) * Math.PI) / 2;
1596
+ }
1597
+ else {
1598
+ pitch = Math.asin(sinp);
1599
+ }
1600
+ const siny_cosp = 2 * (w * y + x * z);
1601
+ const cosy_cosp = 1 - 2 * (x * x + y * y);
1602
+ const yaw = Math.atan2(siny_cosp, cosy_cosp);
1603
+ const sinr_cosp = 2 * (w * z + x * y);
1604
+ const cosr_cosp = 1 - 2 * (x * x + z * z);
1605
+ const roll = Math.atan2(sinr_cosp, cosr_cosp);
1606
+ return {
1607
+ pitch: pitch * RAD_TO_DEG,
1608
+ yaw: yaw * RAD_TO_DEG,
1609
+ roll: roll * RAD_TO_DEG,
1610
+ };
1611
+ }
1612
+ /**
1613
+ * Convert euler angles (in degrees) to quaternion
1614
+ * Uses YXZ order (yaw-pitch-roll) which is standard for XR:
1615
+ * - Yaw: rotation around Y axis (turning left/right)
1616
+ * - Pitch: rotation around X axis (looking up/down)
1617
+ * - Roll: rotation around Z axis (tilting head)
1618
+ * Missing angles default to 0.
1619
+ */
1620
+ function eulerToQuat(euler) {
1621
+ var _a, _b, _c;
1622
+ const DEG_TO_RAD = Math.PI / 180;
1623
+ const pitch = ((_a = euler.pitch) !== null && _a !== void 0 ? _a : 0) * DEG_TO_RAD; // X-axis
1624
+ const yaw = ((_b = euler.yaw) !== null && _b !== void 0 ? _b : 0) * DEG_TO_RAD; // Y-axis
1625
+ const roll = ((_c = euler.roll) !== null && _c !== void 0 ? _c : 0) * DEG_TO_RAD; // Z-axis
1626
+ // Half angles
1627
+ const cx = Math.cos(pitch * 0.5);
1628
+ const sx = Math.sin(pitch * 0.5);
1629
+ const cy = Math.cos(yaw * 0.5);
1630
+ const sy = Math.sin(yaw * 0.5);
1631
+ const cz = Math.cos(roll * 0.5);
1632
+ const sz = Math.sin(roll * 0.5);
1633
+ // YXZ order: first yaw, then pitch, then roll
1634
+ return {
1635
+ w: cx * cy * cz + sx * sy * sz,
1636
+ x: sx * cy * cz + cx * sy * sz,
1637
+ y: cx * sy * cz - sx * cy * sz,
1638
+ z: cx * cy * sz - sx * sy * cz,
1639
+ };
1640
+ }
1641
+ /**
1642
+ * Calculate normalized direction vector from one point to another
1643
+ * Returns default forward direction (0, 0, -1) if points are coincident
1644
+ */
1645
+ function directionTo(from, to) {
1646
+ const dx = to.x - from.x;
1647
+ const dy = to.y - from.y;
1648
+ const dz = to.z - from.z;
1649
+ const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
1650
+ if (length === 0) {
1651
+ return { x: 0, y: 0, z: -1 }; // Default forward (WebXR convention)
1652
+ }
1653
+ return {
1654
+ x: dx / length,
1655
+ y: dy / length,
1656
+ z: dz / length,
1657
+ };
1658
+ }
1659
+ /**
1660
+ * Calculate gimbal-style look rotation (yaw + pitch only, roll = 0)
1661
+ * This keeps the camera/headset level while looking at a target.
1662
+ * @param direction - The direction to look towards
1663
+ * @returns Quaternion with only yaw and pitch, no roll
1664
+ */
1665
+ function lookRotationGimbal(direction) {
1666
+ // Calculate horizontal distance
1667
+ const horizontalDist = Math.sqrt(direction.x * direction.x + direction.z * direction.z);
1668
+ // Calculate yaw: rotation around Y axis to face target horizontally
1669
+ // atan2(-z, -x) gives angle from negative Z axis (forward in WebXR)
1670
+ // We use -z, x to match WebXR's -Z forward convention
1671
+ let yaw = 0;
1672
+ if (horizontalDist > 0.0001) {
1673
+ yaw = Math.atan2(-direction.x, -direction.z);
1674
+ }
1675
+ // Calculate pitch: rotation around X axis to look up/down
1676
+ // Positive direction.y (target above) = positive pitch (look up)
1677
+ // Negative direction.y (target below) = negative pitch (look down)
1678
+ const pitch = Math.atan2(direction.y, horizontalDist);
1679
+ // Convert to degrees and create quaternion (roll = 0)
1680
+ const RAD_TO_DEG = 180 / Math.PI;
1681
+ return eulerToQuat({
1682
+ pitch: pitch * RAD_TO_DEG,
1683
+ yaw: yaw * RAD_TO_DEG,
1684
+ roll: 0,
1685
+ });
1686
+ }
1687
+ /**
1688
+ * Calculate quaternion that looks from origin towards a direction
1689
+ * @param direction - The direction to look towards (will be normalized)
1690
+ * @param up - The up vector (default: world up Y-axis)
1691
+ */
1692
+ function lookRotation(direction, up = { x: 0, y: 1, z: 0 }) {
1693
+ // Normalize direction
1694
+ const dirLen = Math.sqrt(direction.x * direction.x +
1695
+ direction.y * direction.y +
1696
+ direction.z * direction.z);
1697
+ if (dirLen === 0) {
1698
+ return { x: 0, y: 0, z: 0, w: 1 }; // Identity quaternion
1699
+ }
1700
+ const forward = {
1701
+ x: direction.x / dirLen,
1702
+ y: direction.y / dirLen,
1703
+ z: direction.z / dirLen,
1704
+ };
1705
+ // Calculate right vector (cross product of forward and up, NOT up and forward)
1706
+ // forward × up gives correct right-hand orientation
1707
+ const right = {
1708
+ x: forward.y * up.z - forward.z * up.y,
1709
+ y: forward.z * up.x - forward.x * up.z,
1710
+ z: forward.x * up.y - forward.y * up.x,
1711
+ };
1712
+ const rightLen = Math.sqrt(right.x * right.x + right.y * right.y + right.z * right.z);
1713
+ if (rightLen === 0) {
1714
+ // Direction is parallel to up, choose a different up
1715
+ const altUp = { x: 1, y: 0, z: 0 };
1716
+ right.x = forward.y * altUp.z - forward.z * altUp.y;
1717
+ right.y = forward.z * altUp.x - forward.x * altUp.z;
1718
+ right.z = forward.x * altUp.y - forward.y * altUp.x;
1719
+ const altRightLen = Math.sqrt(right.x * right.x + right.y * right.y + right.z * right.z);
1720
+ right.x /= altRightLen;
1721
+ right.y /= altRightLen;
1722
+ right.z /= altRightLen;
1723
+ }
1724
+ else {
1725
+ right.x /= rightLen;
1726
+ right.y /= rightLen;
1727
+ right.z /= rightLen;
1728
+ }
1729
+ // Recalculate up (cross product of right and forward for proper orientation)
1730
+ const newUp = {
1731
+ x: right.y * forward.z - right.z * forward.y,
1732
+ y: right.z * forward.x - right.x * forward.z,
1733
+ z: right.x * forward.y - right.y * forward.x,
1734
+ };
1735
+ // Build rotation matrix and convert to quaternion
1736
+ // Matrix: [right, newUp, -forward] (column vectors)
1737
+ const m00 = right.x, m01 = newUp.x, m02 = -forward.x;
1738
+ const m10 = right.y, m11 = newUp.y, m12 = -forward.y;
1739
+ const m20 = right.z, m21 = newUp.z, m22 = -forward.z;
1740
+ const trace = m00 + m11 + m22;
1741
+ let qw, qx, qy, qz;
1742
+ if (trace > 0) {
1743
+ const s = 0.5 / Math.sqrt(trace + 1.0);
1744
+ qw = 0.25 / s;
1745
+ qx = (m21 - m12) * s;
1746
+ qy = (m02 - m20) * s;
1747
+ qz = (m10 - m01) * s;
1748
+ }
1749
+ else if (m00 > m11 && m00 > m22) {
1750
+ const s = 2.0 * Math.sqrt(1.0 + m00 - m11 - m22);
1751
+ qw = (m21 - m12) / s;
1752
+ qx = 0.25 * s;
1753
+ qy = (m01 + m10) / s;
1754
+ qz = (m02 + m20) / s;
1755
+ }
1756
+ else if (m11 > m22) {
1757
+ const s = 2.0 * Math.sqrt(1.0 + m11 - m00 - m22);
1758
+ qw = (m02 - m20) / s;
1759
+ qx = (m01 + m10) / s;
1760
+ qy = 0.25 * s;
1761
+ qz = (m12 + m21) / s;
1762
+ }
1763
+ else {
1764
+ const s = 2.0 * Math.sqrt(1.0 + m22 - m00 - m11);
1765
+ qw = (m10 - m01) / s;
1766
+ qx = (m02 + m20) / s;
1767
+ qy = (m12 + m21) / s;
1768
+ qz = 0.25 * s;
1769
+ }
1770
+ // Normalize the quaternion to ensure unit length
1771
+ const len = Math.sqrt(qx * qx + qy * qy + qz * qz + qw * qw);
1772
+ if (len > 0) {
1773
+ qx /= len;
1774
+ qy /= len;
1775
+ qz /= len;
1776
+ qw /= len;
1777
+ }
1778
+ return { x: qx, y: qy, z: qz, w: qw };
1779
+ }
1780
+ /**
1781
+ * Wait for a condition to become true, checking each animation frame
1782
+ */
1783
+ function waitForCondition(condition, timeoutMs = 5000) {
1784
+ return new Promise((resolve, reject) => {
1785
+ const startTime = Date.now();
1786
+ const check = () => {
1787
+ if (condition()) {
1788
+ resolve();
1789
+ }
1790
+ else if (Date.now() - startTime > timeoutMs) {
1791
+ reject(new Error('Timeout waiting for condition'));
1792
+ }
1793
+ else {
1794
+ requestAnimationFrame(check);
1795
+ }
1796
+ };
1797
+ check();
1798
+ });
1799
+ }
1800
+
1801
+ /**
1802
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
1803
+ *
1804
+ * This source code is licensed under the MIT license found in the
1805
+ * LICENSE file in the root directory of this source tree.
1806
+ */
1807
+ /**
1808
+ * Check if an orientation input is euler angles (has any of pitch, yaw, or roll)
1809
+ */
1810
+ function isEulerRotation(orientation) {
1811
+ return 'pitch' in orientation || 'yaw' in orientation || 'roll' in orientation;
1812
+ }
1813
+ /**
1814
+ * Normalize an orientation input to a quaternion
1815
+ */
1816
+ function normalizeOrientation(orientation) {
1817
+ if (isEulerRotation(orientation)) {
1818
+ return eulerToQuat(orientation);
1819
+ }
1820
+ return orientation;
1821
+ }
1822
+ /**
1823
+ * Linear interpolation for numbers
1824
+ */
1825
+ function lerp(a, b, t) {
1826
+ return a + (b - a) * t;
1827
+ }
1828
+ /**
1829
+ * Linear interpolation for Vec3
1830
+ */
1831
+ function lerpVec3(a, b, t) {
1832
+ return {
1833
+ x: lerp(a.x, b.x, t),
1834
+ y: lerp(a.y, b.y, t),
1835
+ z: lerp(a.z, b.z, t),
1836
+ };
1837
+ }
1838
+ /**
1839
+ * Spherical linear interpolation for quaternions
1840
+ */
1841
+ function slerpQuat(a, b, t) {
1842
+ // Compute dot product
1843
+ let dot = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
1844
+ // If dot is negative, negate one quaternion to take shorter path
1845
+ let bx = b.x, by = b.y, bz = b.z, bw = b.w;
1846
+ if (dot < 0) {
1847
+ dot = -dot;
1848
+ bx = -bx;
1849
+ by = -by;
1850
+ bz = -bz;
1851
+ bw = -bw;
1852
+ }
1853
+ // If quaternions are very close, use linear interpolation
1854
+ if (dot > 0.9995) {
1855
+ const result = {
1856
+ x: lerp(a.x, bx, t),
1857
+ y: lerp(a.y, by, t),
1858
+ z: lerp(a.z, bz, t),
1859
+ w: lerp(a.w, bw, t),
1860
+ };
1861
+ // Normalize
1862
+ const len = Math.sqrt(result.x * result.x +
1863
+ result.y * result.y +
1864
+ result.z * result.z +
1865
+ result.w * result.w);
1866
+ return {
1867
+ x: result.x / len,
1868
+ y: result.y / len,
1869
+ z: result.z / len,
1870
+ w: result.w / len,
1871
+ };
1872
+ }
1873
+ // Standard slerp
1874
+ const theta0 = Math.acos(dot);
1875
+ const theta = theta0 * t;
1876
+ const sinTheta = Math.sin(theta);
1877
+ const sinTheta0 = Math.sin(theta0);
1878
+ const s0 = Math.cos(theta) - (dot * sinTheta) / sinTheta0;
1879
+ const s1 = sinTheta / sinTheta0;
1880
+ return {
1881
+ x: s0 * a.x + s1 * bx,
1882
+ y: s0 * a.y + s1 * by,
1883
+ z: s0 * a.z + s1 * bz,
1884
+ w: s0 * a.w + s1 * bw,
1885
+ };
1886
+ }
1887
+ /**
1888
+ * Map of common device name aliases to canonical DeviceId values.
1889
+ * Enables callers to use natural variants like "right", "left-controller",
1890
+ * "controllers.right", etc. in addition to the canonical names.
1891
+ */
1892
+ const DEVICE_ID_ALIASES = {
1893
+ right: 'controller-right',
1894
+ left: 'controller-left',
1895
+ 'right-controller': 'controller-right',
1896
+ 'left-controller': 'controller-left',
1897
+ 'controllers.right': 'controller-right',
1898
+ 'controllers.left': 'controller-left',
1899
+ rightController: 'controller-right',
1900
+ leftController: 'controller-left',
1901
+ 'right-hand': 'hand-right',
1902
+ 'left-hand': 'hand-left',
1903
+ 'hands.right': 'hand-right',
1904
+ 'hands.left': 'hand-left',
1905
+ rightHand: 'hand-right',
1906
+ leftHand: 'hand-left',
1907
+ };
1908
+ /**
1909
+ * Resolve a device identifier, accepting both canonical names and common aliases.
1910
+ */
1911
+ function resolveDeviceId(id) {
1912
+ var _a;
1913
+ return (_a = DEVICE_ID_ALIASES[id]) !== null && _a !== void 0 ? _a : id;
1914
+ }
1915
+ /**
1916
+ * RemoteControlInterface provides frame-synchronized programmatic control of an XRDevice.
1917
+ *
1918
+ * This class implements a command queue that processes actions during each frame update,
1919
+ * enabling smooth animations and coordinated control with DevUI.
1920
+ *
1921
+ * Key features:
1922
+ * - Frame-synchronized execution: Commands are queued and processed during frame update
1923
+ * - Duration-based actions: Smooth animations via lerp over multiple frames
1924
+ * - Automatic capture/release: Captures device on first command, releases 30s after queue empties
1925
+ * - Unified device identifiers: 'headset', 'controller-left', 'hand-right', etc.
1926
+ *
1927
+ * Usage:
1928
+ * ```typescript
1929
+ * import { XRDevice, metaQuest3 } from 'iwer';
1930
+ *
1931
+ * const device = new XRDevice(metaQuest3);
1932
+ * device.installRuntime();
1933
+ *
1934
+ * // Get transform
1935
+ * const result = await device.remote.dispatch('get_transform', { device: 'headset' });
1936
+ *
1937
+ * // Animate headset to new position over 1 second
1938
+ * await device.remote.dispatch('animate_to', {
1939
+ * device: 'headset',
1940
+ * position: { x: 0, y: 1.6, z: -1 },
1941
+ * duration: 1.0
1942
+ * });
1943
+ * ```
1944
+ */
1945
+ class RemoteControlInterface {
1946
+ constructor(device) {
1947
+ this.commandQueue = [];
1948
+ this._isCaptured = false;
1949
+ this.releaseTimer = null;
1950
+ this.actionIdCounter = 0;
1951
+ /** Release timeout in milliseconds (default: 30000 = 30 seconds) */
1952
+ this.RELEASE_TIMEOUT_MS = 30000;
1953
+ this.device = device;
1954
+ }
1955
+ generateActionId() {
1956
+ return `action_${++this.actionIdCounter}`;
1957
+ }
1958
+ // =============================================================================
1959
+ // Public Properties
1960
+ // =============================================================================
1961
+ /**
1962
+ * Whether the device is currently captured for programmatic control.
1963
+ * When true, DevUI should go into passive mode (sync FROM device only).
1964
+ */
1965
+ get isCaptured() {
1966
+ return this._isCaptured;
1967
+ }
1968
+ /**
1969
+ * Number of pending actions in the queue
1970
+ */
1971
+ get queueLength() {
1972
+ return this.commandQueue.length;
1973
+ }
1974
+ // =============================================================================
1975
+ // Queue Management
1976
+ // =============================================================================
1977
+ /**
1978
+ * Enqueue a discrete action for processing
1979
+ */
1980
+ enqueueDiscrete(method, params) {
1981
+ return new Promise((resolve, reject) => {
1982
+ const action = {
1983
+ type: 'discrete',
1984
+ id: this.generateActionId(),
1985
+ method,
1986
+ params,
1987
+ resolve,
1988
+ reject,
1989
+ };
1990
+ this.commandQueue.push(action);
1991
+ });
1992
+ }
1993
+ /**
1994
+ * Enqueue a duration action for processing
1995
+ */
1996
+ enqueueDuration(method, params, durationMs, startState, targetState) {
1997
+ return new Promise((resolve, reject) => {
1998
+ const action = {
1999
+ type: 'duration',
2000
+ id: this.generateActionId(),
2001
+ method,
2002
+ params,
2003
+ durationMs,
2004
+ elapsedMs: 0,
2005
+ startState,
2006
+ targetState,
2007
+ resolve,
2008
+ reject,
2009
+ };
2010
+ this.commandQueue.push(action);
2011
+ });
2012
+ }
2013
+ /**
2014
+ * Update method called each frame by XRDevice.
2015
+ * Processes the command queue and handles duration-based animations.
2016
+ *
2017
+ * @param deltaTimeMs - Time since last frame in milliseconds
2018
+ */
2019
+ update(deltaTimeMs) {
2020
+ if (this.commandQueue.length === 0) {
2021
+ return;
2022
+ }
2023
+ // Always cancel pending release while queue is active
2024
+ this.cancelReleaseTimer();
2025
+ // Activate capture mode
2026
+ if (!this._isCaptured) {
2027
+ this._isCaptured = true;
2028
+ this.device.controlMode = 'programmatic';
2029
+ }
2030
+ while (this.commandQueue.length > 0) {
2031
+ const action = this.commandQueue[0];
2032
+ if (action.type === 'discrete') {
2033
+ // Execute discrete action immediately
2034
+ try {
2035
+ const result = this.executeDiscreteAction(action);
2036
+ action.resolve(result);
2037
+ }
2038
+ catch (error) {
2039
+ action.reject(error);
2040
+ }
2041
+ this.commandQueue.shift();
2042
+ // Continue to next action
2043
+ }
2044
+ else {
2045
+ // Duration action - lerp by delta time
2046
+ action.elapsedMs += deltaTimeMs;
2047
+ if (action.elapsedMs >= action.durationMs) {
2048
+ // Complete - apply final state
2049
+ try {
2050
+ this.applyDurationFinalState(action);
2051
+ action.resolve(this.getDurationResult(action));
2052
+ }
2053
+ catch (error) {
2054
+ action.reject(error);
2055
+ }
2056
+ this.commandQueue.shift();
2057
+ // Continue to next action
2058
+ }
2059
+ else {
2060
+ // In progress - lerp
2061
+ try {
2062
+ const t = action.elapsedMs / action.durationMs;
2063
+ this.applyDurationLerpState(action, t);
2064
+ }
2065
+ catch (error) {
2066
+ action.reject(error);
2067
+ this.commandQueue.shift();
2068
+ continue;
2069
+ }
2070
+ // Stop processing - wait for next frame
2071
+ break;
2072
+ }
2073
+ }
2074
+ }
2075
+ // Notify state change
2076
+ this.device.notifyStateChange();
2077
+ // Start release timer if queue is empty
2078
+ if (this.commandQueue.length === 0) {
2079
+ this.startReleaseTimer();
2080
+ }
2081
+ }
2082
+ startReleaseTimer() {
2083
+ this.cancelReleaseTimer();
2084
+ this.releaseTimer = setTimeout(() => {
2085
+ this._isCaptured = false;
2086
+ this.device.controlMode = 'manual';
2087
+ this.releaseTimer = null;
2088
+ }, this.RELEASE_TIMEOUT_MS);
2089
+ }
2090
+ cancelReleaseTimer() {
2091
+ if (this.releaseTimer !== null) {
2092
+ clearTimeout(this.releaseTimer);
2093
+ this.releaseTimer = null;
2094
+ }
2095
+ }
2096
+ // =============================================================================
2097
+ // Device Resolution
2098
+ // =============================================================================
2099
+ /**
2100
+ * Get the transform (position, quaternion) for a device
2101
+ */
2102
+ getDeviceTransform(deviceId) {
2103
+ switch (deviceId) {
2104
+ case 'headset':
2105
+ return {
2106
+ position: vec3ToObj(this.device.position),
2107
+ orientation: quatToObj(this.device.quaternion),
2108
+ };
2109
+ case 'controller-left': {
2110
+ const controller = this.device.controllers.left;
2111
+ if (!controller)
2112
+ throw new Error('Left controller not available');
2113
+ return {
2114
+ position: vec3ToObj(controller.position),
2115
+ orientation: quatToObj(controller.quaternion),
2116
+ };
2117
+ }
2118
+ case 'controller-right': {
2119
+ const controller = this.device.controllers.right;
2120
+ if (!controller)
2121
+ throw new Error('Right controller not available');
2122
+ return {
2123
+ position: vec3ToObj(controller.position),
2124
+ orientation: quatToObj(controller.quaternion),
2125
+ };
2126
+ }
2127
+ case 'hand-left': {
2128
+ const hand = this.device.hands.left;
2129
+ if (!hand)
2130
+ throw new Error('Left hand not available');
2131
+ return {
2132
+ position: vec3ToObj(hand.position),
2133
+ orientation: quatToObj(hand.quaternion),
2134
+ };
2135
+ }
2136
+ case 'hand-right': {
2137
+ const hand = this.device.hands.right;
2138
+ if (!hand)
2139
+ throw new Error('Right hand not available');
2140
+ return {
2141
+ position: vec3ToObj(hand.position),
2142
+ orientation: quatToObj(hand.quaternion),
2143
+ };
2144
+ }
2145
+ default:
2146
+ throw new Error(`Unknown device: ${deviceId}`);
2147
+ }
2148
+ }
2149
+ /**
2150
+ * Set the transform for a device
2151
+ */
2152
+ setDeviceTransform(deviceId, position, orientation) {
2153
+ switch (deviceId) {
2154
+ case 'headset':
2155
+ if (position) {
2156
+ this.device.position.set(position.x, position.y, position.z);
2157
+ }
2158
+ if (orientation) {
2159
+ this.device.quaternion.set(orientation.x, orientation.y, orientation.z, orientation.w);
2160
+ }
2161
+ break;
2162
+ case 'controller-left': {
2163
+ const controller = this.device.controllers.left;
2164
+ if (!controller)
2165
+ throw new Error('Left controller not available');
2166
+ if (position) {
2167
+ controller.position.set(position.x, position.y, position.z);
2168
+ }
2169
+ if (orientation) {
2170
+ controller.quaternion.set(orientation.x, orientation.y, orientation.z, orientation.w);
2171
+ }
2172
+ break;
2173
+ }
2174
+ case 'controller-right': {
2175
+ const controller = this.device.controllers.right;
2176
+ if (!controller)
2177
+ throw new Error('Right controller not available');
2178
+ if (position) {
2179
+ controller.position.set(position.x, position.y, position.z);
2180
+ }
2181
+ if (orientation) {
2182
+ controller.quaternion.set(orientation.x, orientation.y, orientation.z, orientation.w);
2183
+ }
2184
+ break;
2185
+ }
2186
+ case 'hand-left': {
2187
+ const hand = this.device.hands.left;
2188
+ if (!hand)
2189
+ throw new Error('Left hand not available');
2190
+ if (position) {
2191
+ hand.position.set(position.x, position.y, position.z);
2192
+ }
2193
+ if (orientation) {
2194
+ hand.quaternion.set(orientation.x, orientation.y, orientation.z, orientation.w);
2195
+ }
2196
+ break;
2197
+ }
2198
+ case 'hand-right': {
2199
+ const hand = this.device.hands.right;
2200
+ if (!hand)
2201
+ throw new Error('Right hand not available');
2202
+ if (position) {
2203
+ hand.position.set(position.x, position.y, position.z);
2204
+ }
2205
+ if (orientation) {
2206
+ hand.quaternion.set(orientation.x, orientation.y, orientation.z, orientation.w);
2207
+ }
2208
+ break;
2209
+ }
2210
+ default:
2211
+ throw new Error(`Unknown device: ${deviceId}`);
2212
+ }
2213
+ }
2214
+ /**
2215
+ * Transform a position from XR-origin-relative coordinates to GlobalSpace.
2216
+ * The XR origin is defined by the first reference space requested by the app.
2217
+ * This is necessary because device positions are in GlobalSpace, but positions
2218
+ * from get_object_transform are relative to the XR origin.
2219
+ */
2220
+ transformXROriginToGlobal(position) {
2221
+ var _a, _b;
2222
+ const session = this.device.activeSession;
2223
+ if (!session) {
2224
+ return position;
2225
+ }
2226
+ const refSpaces = (_a = session[P_SESSION]) === null || _a === void 0 ? void 0 : _a.referenceSpaces;
2227
+ if (!refSpaces || refSpaces.length === 0) {
2228
+ return position;
2229
+ }
2230
+ // Use the first reference space (primary one requested by app)
2231
+ const primaryRefSpace = refSpaces[0];
2232
+ const offsetMatrix = (_b = primaryRefSpace[P_SPACE]) === null || _b === void 0 ? void 0 : _b.offsetMatrix;
2233
+ if (!offsetMatrix) {
2234
+ return position;
2235
+ }
2236
+ // Transform position from XR-origin space to GlobalSpace
2237
+ const posVec = fromValues$2(position.x, position.y, position.z);
2238
+ transformMat4$1(posVec, posVec, offsetMatrix);
2239
+ return {
2240
+ x: posVec[0],
2241
+ y: posVec[1],
2242
+ z: posVec[2],
2243
+ };
2244
+ }
2245
+ /**
2246
+ * Get the select value for an input device (trigger for controller, pinch for hand)
2247
+ */
2248
+ getDeviceSelectValue(deviceId) {
2249
+ var _a, _b, _c, _d, _e, _f, _g, _h;
2250
+ switch (deviceId) {
2251
+ case 'controller-left':
2252
+ return (_b = (_a = this.device.controllers.left) === null || _a === void 0 ? void 0 : _a.getButtonValue('trigger')) !== null && _b !== void 0 ? _b : 0;
2253
+ case 'controller-right':
2254
+ return (_d = (_c = this.device.controllers.right) === null || _c === void 0 ? void 0 : _c.getButtonValue('trigger')) !== null && _d !== void 0 ? _d : 0;
2255
+ case 'hand-left':
2256
+ return (_f = (_e = this.device.hands.left) === null || _e === void 0 ? void 0 : _e.pinchValue) !== null && _f !== void 0 ? _f : 0;
2257
+ case 'hand-right':
2258
+ return (_h = (_g = this.device.hands.right) === null || _g === void 0 ? void 0 : _g.pinchValue) !== null && _h !== void 0 ? _h : 0;
2259
+ default:
2260
+ throw new Error(`Unknown input device: ${deviceId}`);
2261
+ }
2262
+ }
2263
+ /**
2264
+ * Set the select value for an input device
2265
+ */
2266
+ setDeviceSelectValue(deviceId, value) {
2267
+ var _a, _b, _c, _d;
2268
+ switch (deviceId) {
2269
+ case 'controller-left':
2270
+ (_a = this.device.controllers.left) === null || _a === void 0 ? void 0 : _a.updateButtonValue('trigger', value);
2271
+ break;
2272
+ case 'controller-right':
2273
+ (_b = this.device.controllers.right) === null || _b === void 0 ? void 0 : _b.updateButtonValue('trigger', value);
2274
+ break;
2275
+ case 'hand-left':
2276
+ (_c = this.device.hands.left) === null || _c === void 0 ? void 0 : _c.updatePinchValue(value);
2277
+ break;
2278
+ case 'hand-right':
2279
+ (_d = this.device.hands.right) === null || _d === void 0 ? void 0 : _d.updatePinchValue(value);
2280
+ break;
2281
+ default:
2282
+ throw new Error(`Unknown input device: ${deviceId}`);
2283
+ }
2284
+ }
2285
+ /**
2286
+ * Set connected state for an input device
2287
+ */
2288
+ setDeviceConnected(deviceId, connected) {
2289
+ switch (deviceId) {
2290
+ case 'controller-left':
2291
+ if (this.device.controllers.left) {
2292
+ this.device.controllers.left.connected = connected;
2293
+ }
2294
+ break;
2295
+ case 'controller-right':
2296
+ if (this.device.controllers.right) {
2297
+ this.device.controllers.right.connected = connected;
2298
+ }
2299
+ break;
2300
+ case 'hand-left':
2301
+ if (this.device.hands.left) {
2302
+ this.device.hands.left.connected = connected;
2303
+ }
2304
+ break;
2305
+ case 'hand-right':
2306
+ if (this.device.hands.right) {
2307
+ this.device.hands.right.connected = connected;
2308
+ }
2309
+ break;
2310
+ default:
2311
+ throw new Error(`Unknown input device: ${deviceId}`);
2312
+ }
2313
+ }
2314
+ // =============================================================================
2315
+ // Discrete Action Execution
2316
+ // =============================================================================
2317
+ executeDiscreteAction(action) {
2318
+ const { method, params } = action;
2319
+ switch (method) {
2320
+ // Session tools
2321
+ case 'get_session_status':
2322
+ return this.executeGetSessionStatus();
2323
+ case 'accept_session':
2324
+ return this.executeAcceptSession();
2325
+ case 'end_session':
2326
+ return this.executeEndSession();
2327
+ // Transform tools
2328
+ case 'get_transform':
2329
+ return this.executeGetTransform(params);
2330
+ case 'set_transform':
2331
+ return this.executeSetTransform(params);
2332
+ case 'look_at':
2333
+ return this.executeLookAt(params);
2334
+ // Input tools
2335
+ case 'set_input_mode':
2336
+ return this.executeSetInputMode(params);
2337
+ case 'set_connected':
2338
+ return this.executeSetConnected(params);
2339
+ case 'get_select_value':
2340
+ return this.executeGetSelectValue(params);
2341
+ case 'set_select_value':
2342
+ return this.executeSetSelectValue(params);
2343
+ // Gamepad tools
2344
+ case 'get_gamepad_state':
2345
+ return this.executeGetGamepadState(params);
2346
+ case 'set_gamepad_state':
2347
+ return this.executeSetGamepadState(params);
2348
+ // State tools
2349
+ case 'get_device_state':
2350
+ return this.executeGetDeviceState();
2351
+ case 'set_device_state':
2352
+ return this.executeSetDeviceState(params);
2353
+ case 'capture_canvas':
2354
+ return this.executeCaptureCanvas(params);
2355
+ // Internal select sequence actions
2356
+ case '_select_press': {
2357
+ const deviceId = params.device;
2358
+ this.setDeviceSelectValue(deviceId, 1);
2359
+ return undefined;
2360
+ }
2361
+ case '_select_release': {
2362
+ const deviceId = params.device;
2363
+ this.setDeviceSelectValue(deviceId, 0);
2364
+ return undefined;
2365
+ }
2366
+ default:
2367
+ throw new Error(`Unknown method: ${method}`);
2368
+ }
2369
+ }
2370
+ // =============================================================================
2371
+ // Session Tool Implementations
2372
+ // =============================================================================
2373
+ executeGetSessionStatus() {
2374
+ const session = this.device.activeSession;
2375
+ return {
2376
+ deviceName: this.device.name,
2377
+ isRuntimeInstalled: true,
2378
+ sessionActive: !!session,
2379
+ sessionOffered: this.device.sessionOffered,
2380
+ sessionMode: session ? session.mode : null,
2381
+ enabledFeatures: session
2382
+ ? Array.from(session.enabledFeatures || [])
2383
+ : [],
2384
+ visibilityState: this.device.visibilityState,
2385
+ };
2386
+ }
2387
+ executeAcceptSession() {
2388
+ if (!this.device.sessionOffered) {
2389
+ throw new Error('No session has been offered');
2390
+ }
2391
+ this.device.grantOfferedSession();
2392
+ // Session activation is async - caller should use get_session_status to poll
2393
+ return { success: true };
2394
+ }
2395
+ executeEndSession() {
2396
+ const session = this.device.activeSession;
2397
+ if (!session) {
2398
+ throw new Error('No active session');
2399
+ }
2400
+ session.end();
2401
+ return { success: true };
2402
+ }
2403
+ // =============================================================================
2404
+ // Transform Tool Implementations
2405
+ // =============================================================================
2406
+ executeGetTransform(params) {
2407
+ const { device: deviceId } = params;
2408
+ const transform = this.getDeviceTransform(deviceId);
2409
+ return {
2410
+ device: deviceId,
2411
+ position: transform.position,
2412
+ orientation: transform.orientation,
2413
+ euler: quatToEuler(transform.orientation),
2414
+ };
2415
+ }
2416
+ executeSetTransform(params) {
2417
+ const { device: deviceId, position, orientation } = params;
2418
+ const targetOrientation = orientation
2419
+ ? normalizeOrientation(orientation)
2420
+ : undefined;
2421
+ this.setDeviceTransform(deviceId, position, targetOrientation);
2422
+ const newTransform = this.getDeviceTransform(deviceId);
2423
+ return {
2424
+ device: deviceId,
2425
+ position: newTransform.position,
2426
+ orientation: newTransform.orientation,
2427
+ };
2428
+ }
2429
+ executeLookAt(params) {
2430
+ const { device: deviceId, target, moveToDistance } = params;
2431
+ const currentTransform = this.getDeviceTransform(deviceId);
2432
+ // Transform target from XR-origin-relative to GlobalSpace
2433
+ const targetInGlobal = this.transformXROriginToGlobal(target);
2434
+ // Calculate direction to target
2435
+ const direction = directionTo(currentTransform.position, targetInGlobal);
2436
+ // Calculate look rotation
2437
+ // Use gimbal rotation for headset (keeps it level, no roll)
2438
+ // Use standard lookRotation for controllers/hands (can tilt freely)
2439
+ const lookQuat = deviceId === 'headset'
2440
+ ? lookRotationGimbal(direction)
2441
+ : lookRotation(direction);
2442
+ // Optionally move to a specific distance from target
2443
+ let newPosition;
2444
+ if (moveToDistance !== undefined) {
2445
+ newPosition = {
2446
+ x: targetInGlobal.x - direction.x * moveToDistance,
2447
+ y: targetInGlobal.y - direction.y * moveToDistance,
2448
+ z: targetInGlobal.z - direction.z * moveToDistance,
2449
+ };
2450
+ }
2451
+ this.setDeviceTransform(deviceId, newPosition, lookQuat);
2452
+ const newTransform = this.getDeviceTransform(deviceId);
2453
+ return {
2454
+ device: deviceId,
2455
+ position: newTransform.position,
2456
+ orientation: newTransform.orientation,
2457
+ };
2458
+ }
2459
+ // =============================================================================
2460
+ // Input Tool Implementations
2461
+ // =============================================================================
2462
+ executeSetInputMode(params) {
2463
+ var _a, _b, _c, _d;
2464
+ const { mode } = params;
2465
+ this.device.primaryInputMode = mode;
2466
+ const activeDevices = [];
2467
+ if (mode === 'controller') {
2468
+ if ((_a = this.device.controllers.left) === null || _a === void 0 ? void 0 : _a.connected) {
2469
+ activeDevices.push('controller-left');
2470
+ }
2471
+ if ((_b = this.device.controllers.right) === null || _b === void 0 ? void 0 : _b.connected) {
2472
+ activeDevices.push('controller-right');
2473
+ }
2474
+ }
2475
+ else {
2476
+ if ((_c = this.device.hands.left) === null || _c === void 0 ? void 0 : _c.connected) {
2477
+ activeDevices.push('hand-left');
2478
+ }
2479
+ if ((_d = this.device.hands.right) === null || _d === void 0 ? void 0 : _d.connected) {
2480
+ activeDevices.push('hand-right');
2481
+ }
2482
+ }
2483
+ return { mode, activeDevices };
2484
+ }
2485
+ executeSetConnected(params) {
2486
+ const { device: deviceId, connected } = params;
2487
+ this.setDeviceConnected(deviceId, connected);
2488
+ return { device: deviceId, connected };
2489
+ }
2490
+ executeGetSelectValue(params) {
2491
+ const { device: deviceId } = params;
2492
+ const value = this.getDeviceSelectValue(deviceId);
2493
+ return { device: deviceId, value };
2494
+ }
2495
+ executeSetSelectValue(params) {
2496
+ const { device: deviceId, value } = params;
2497
+ this.setDeviceSelectValue(deviceId, value);
2498
+ return { device: deviceId, value };
2499
+ }
2500
+ // =============================================================================
2501
+ // Gamepad Tool Implementations
2502
+ // =============================================================================
2503
+ executeGetGamepadState(params) {
2504
+ const { device: deviceId } = params;
2505
+ const hand = deviceId === 'controller-left' ? 'left' : 'right';
2506
+ const controller = this.device.controllers[hand];
2507
+ if (!controller) {
2508
+ throw new Error(`Controller ${hand} not available`);
2509
+ }
2510
+ // Button layout for Meta Quest Touch Plus controllers
2511
+ // Use hand-conditional internal names for lookup
2512
+ const buttonInternalNames = [
2513
+ 'trigger',
2514
+ 'squeeze',
2515
+ 'thumbstick',
2516
+ hand === 'left' ? 'x-button' : 'a-button',
2517
+ hand === 'left' ? 'y-button' : 'b-button',
2518
+ 'thumbrest',
2519
+ ];
2520
+ const buttons = buttonInternalNames.map((name, index) => ({
2521
+ index,
2522
+ name: name
2523
+ .replace('x-button', 'x')
2524
+ .replace('y-button', 'y')
2525
+ .replace('a-button', 'a')
2526
+ .replace('b-button', 'b'),
2527
+ value: controller.getButtonValue(name),
2528
+ touched: controller.getButtonTouched(name),
2529
+ pressed: controller.getButtonValue(name) > 0.5,
2530
+ }));
2531
+ const axesData = controller.getAxes();
2532
+ const axes = [
2533
+ { index: 0, name: 'thumbstick-x', value: axesData.x },
2534
+ { index: 1, name: 'thumbstick-y', value: axesData.y },
2535
+ ];
2536
+ return {
2537
+ device: deviceId,
2538
+ connected: controller.connected,
2539
+ buttons,
2540
+ axes,
2541
+ };
2542
+ }
2543
+ executeSetGamepadState(params) {
2544
+ const { device: deviceId, buttons, axes } = params;
2545
+ const hand = deviceId === 'controller-left' ? 'left' : 'right';
2546
+ const controller = this.device.controllers[hand];
2547
+ if (!controller) {
2548
+ throw new Error(`Controller ${hand} not available`);
2549
+ }
2550
+ let buttonsSet = 0;
2551
+ let axesSet = 0;
2552
+ // Button index to name mapping
2553
+ const buttonIndexToName = [
2554
+ 'trigger',
2555
+ 'squeeze',
2556
+ 'thumbstick',
2557
+ hand === 'left' ? 'x-button' : 'a-button',
2558
+ hand === 'left' ? 'y-button' : 'b-button',
2559
+ 'thumbrest',
2560
+ ];
2561
+ if (buttons) {
2562
+ for (const btn of buttons) {
2563
+ const buttonName = buttonIndexToName[btn.index];
2564
+ if (buttonName) {
2565
+ // Use updateButtonValue for proper event triggering
2566
+ controller.updateButtonValue(buttonName, btn.value);
2567
+ if (btn.touched !== undefined) {
2568
+ controller.updateButtonTouch(buttonName, btn.touched);
2569
+ }
2570
+ buttonsSet++;
2571
+ }
2572
+ }
2573
+ }
2574
+ if (axes) {
2575
+ let xValue;
2576
+ let yValue;
2577
+ for (const axis of axes) {
2578
+ if (axis.index === 0) {
2579
+ xValue = axis.value;
2580
+ axesSet++;
2581
+ }
2582
+ else if (axis.index === 1) {
2583
+ yValue = axis.value;
2584
+ axesSet++;
2585
+ }
2586
+ }
2587
+ if (xValue !== undefined || yValue !== undefined) {
2588
+ const currentAxes = controller.getAxes();
2589
+ controller.updateAxes('thumbstick', xValue !== null && xValue !== void 0 ? xValue : currentAxes.x, yValue !== null && yValue !== void 0 ? yValue : currentAxes.y);
2590
+ }
2591
+ }
2592
+ return { device: deviceId, buttonsSet, axesSet };
2593
+ }
2594
+ // =============================================================================
2595
+ // State Tool Implementations
2596
+ // =============================================================================
2597
+ executeGetDeviceState() {
2598
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
2599
+ return {
2600
+ headset: {
2601
+ position: vec3ToObj(this.device.position),
2602
+ orientation: quatToObj(this.device.quaternion),
2603
+ },
2604
+ inputMode: this.device.primaryInputMode,
2605
+ controllers: {
2606
+ left: {
2607
+ connected: (_b = (_a = this.device.controllers.left) === null || _a === void 0 ? void 0 : _a.connected) !== null && _b !== void 0 ? _b : false,
2608
+ position: vec3ToObj((_d = (_c = this.device.controllers.left) === null || _c === void 0 ? void 0 : _c.position) !== null && _d !== void 0 ? _d : { x: 0, y: 0, z: 0 }),
2609
+ orientation: quatToObj((_f = (_e = this.device.controllers.left) === null || _e === void 0 ? void 0 : _e.quaternion) !== null && _f !== void 0 ? _f : {
2610
+ x: 0,
2611
+ y: 0,
2612
+ z: 0,
2613
+ w: 1,
2614
+ }),
2615
+ },
2616
+ right: {
2617
+ connected: (_h = (_g = this.device.controllers.right) === null || _g === void 0 ? void 0 : _g.connected) !== null && _h !== void 0 ? _h : false,
2618
+ position: vec3ToObj((_k = (_j = this.device.controllers.right) === null || _j === void 0 ? void 0 : _j.position) !== null && _k !== void 0 ? _k : { x: 0, y: 0, z: 0 }),
2619
+ orientation: quatToObj((_m = (_l = this.device.controllers.right) === null || _l === void 0 ? void 0 : _l.quaternion) !== null && _m !== void 0 ? _m : {
2620
+ x: 0,
2621
+ y: 0,
2622
+ z: 0,
2623
+ w: 1,
2624
+ }),
2625
+ },
2626
+ },
2627
+ hands: {
2628
+ left: {
2629
+ connected: (_p = (_o = this.device.hands.left) === null || _o === void 0 ? void 0 : _o.connected) !== null && _p !== void 0 ? _p : false,
2630
+ position: vec3ToObj((_r = (_q = this.device.hands.left) === null || _q === void 0 ? void 0 : _q.position) !== null && _r !== void 0 ? _r : { x: 0, y: 0, z: 0 }),
2631
+ orientation: quatToObj((_t = (_s = this.device.hands.left) === null || _s === void 0 ? void 0 : _s.quaternion) !== null && _t !== void 0 ? _t : { x: 0, y: 0, z: 0, w: 1 }),
2632
+ },
2633
+ right: {
2634
+ connected: (_v = (_u = this.device.hands.right) === null || _u === void 0 ? void 0 : _u.connected) !== null && _v !== void 0 ? _v : false,
2635
+ position: vec3ToObj((_x = (_w = this.device.hands.right) === null || _w === void 0 ? void 0 : _w.position) !== null && _x !== void 0 ? _x : { x: 0, y: 0, z: 0 }),
2636
+ orientation: quatToObj((_z = (_y = this.device.hands.right) === null || _y === void 0 ? void 0 : _y.quaternion) !== null && _z !== void 0 ? _z : { x: 0, y: 0, z: 0, w: 1 }),
2637
+ },
2638
+ },
2639
+ stereoEnabled: this.device.stereoEnabled,
2640
+ fov: this.device.fovy * (180 / Math.PI), // Convert to degrees
2641
+ };
2642
+ }
2643
+ executeSetDeviceState(params) {
2644
+ const { state } = params;
2645
+ if (!state) {
2646
+ // Reset to initial state
2647
+ this.device.position.set(0, 1.6, 0);
2648
+ this.device.quaternion.set(0, 0, 0, 1);
2649
+ this.device.primaryInputMode = 'controller';
2650
+ this.device.stereoEnabled = false;
2651
+ // Reset controllers and hands to default positions
2652
+ if (this.device.controllers.left) {
2653
+ this.device.controllers.left.position.set(-0.2, 1.4, -0.3);
2654
+ this.device.controllers.left.quaternion.set(0, 0, 0, 1);
2655
+ this.device.controllers.left.connected = true;
2656
+ }
2657
+ if (this.device.controllers.right) {
2658
+ this.device.controllers.right.position.set(0.2, 1.4, -0.3);
2659
+ this.device.controllers.right.quaternion.set(0, 0, 0, 1);
2660
+ this.device.controllers.right.connected = true;
2661
+ }
2662
+ if (this.device.hands.left) {
2663
+ this.device.hands.left.position.set(-0.15, 1.3, -0.4);
2664
+ this.device.hands.left.quaternion.set(0, 0, 0, 1);
2665
+ this.device.hands.left.connected = true;
2666
+ }
2667
+ if (this.device.hands.right) {
2668
+ this.device.hands.right.position.set(0.15, 1.3, -0.4);
2669
+ this.device.hands.right.quaternion.set(0, 0, 0, 1);
2670
+ this.device.hands.right.connected = true;
2671
+ }
2672
+ }
2673
+ else {
2674
+ // Apply partial state
2675
+ if (state.headset) {
2676
+ if (state.headset.position) {
2677
+ this.device.position.set(state.headset.position.x, state.headset.position.y, state.headset.position.z);
2678
+ }
2679
+ if (state.headset.orientation) {
2680
+ this.device.quaternion.set(state.headset.orientation.x, state.headset.orientation.y, state.headset.orientation.z, state.headset.orientation.w);
2681
+ }
2682
+ }
2683
+ if (state.inputMode !== undefined) {
2684
+ this.device.primaryInputMode = state.inputMode;
2685
+ }
2686
+ if (state.stereoEnabled !== undefined) {
2687
+ this.device.stereoEnabled = state.stereoEnabled;
2688
+ }
2689
+ if (state.fov !== undefined) {
2690
+ this.device.fovy = state.fov * (Math.PI / 180); // Convert to radians
2691
+ }
2692
+ if (state.controllers) {
2693
+ this.applyInputState('controller-left', state.controllers.left);
2694
+ this.applyInputState('controller-right', state.controllers.right);
2695
+ }
2696
+ if (state.hands) {
2697
+ this.applyInputState('hand-left', state.hands.left);
2698
+ this.applyInputState('hand-right', state.hands.right);
2699
+ }
2700
+ }
2701
+ return { state: this.executeGetDeviceState() };
2702
+ }
2703
+ applyInputState(deviceId, state) {
2704
+ if (!state)
2705
+ return;
2706
+ if (state.connected !== undefined) {
2707
+ this.setDeviceConnected(deviceId, state.connected);
2708
+ }
2709
+ if (state.position || state.orientation) {
2710
+ this.setDeviceTransform(deviceId, state.position, state.orientation);
2711
+ }
2712
+ }
2713
+ executeCaptureCanvas(params) {
2714
+ const { maxWidth = 800, format = 'png', quality = 0.92 } = params;
2715
+ // Get the app canvas - try device first, then fallback to DOM query
2716
+ let canvas = this.device.appCanvas;
2717
+ if (!canvas) {
2718
+ // No active session - try to find the canvas in the DOM
2719
+ // Before XR session, only the app's canvas is in the DOM
2720
+ // (IWER's canvases are not added until session starts)
2721
+ const canvases = document.querySelectorAll('canvas');
2722
+ if (canvases.length === 1) {
2723
+ canvas = canvases[0];
2724
+ }
2725
+ else if (canvases.length > 1) {
2726
+ // Multiple canvases - try to find the most likely app canvas
2727
+ // Prefer the largest visible canvas
2728
+ let bestCanvas = null;
2729
+ let bestArea = 0;
2730
+ canvases.forEach((c) => {
2731
+ const rect = c.getBoundingClientRect();
2732
+ const area = rect.width * rect.height;
2733
+ if (area > bestArea && rect.width > 0 && rect.height > 0) {
2734
+ bestArea = area;
2735
+ bestCanvas = c;
2736
+ }
2737
+ });
2738
+ canvas = bestCanvas;
2739
+ }
2740
+ }
2741
+ if (!canvas) {
2742
+ throw new Error('No canvas available. Either start an XR session or ensure an app canvas is in the DOM.');
2743
+ }
2744
+ // Create a temporary canvas for scaling
2745
+ const tempCanvas = document.createElement('canvas');
2746
+ const ctx = tempCanvas.getContext('2d');
2747
+ if (!ctx) {
2748
+ throw new Error('Failed to create canvas context');
2749
+ }
2750
+ // Calculate scaled dimensions
2751
+ const aspectRatio = canvas.height / canvas.width;
2752
+ const targetWidth = Math.min(canvas.width, maxWidth);
2753
+ const targetHeight = Math.round(targetWidth * aspectRatio);
2754
+ tempCanvas.width = targetWidth;
2755
+ tempCanvas.height = targetHeight;
2756
+ // Draw scaled image
2757
+ ctx.drawImage(canvas, 0, 0, targetWidth, targetHeight);
2758
+ // Convert to base64
2759
+ const mimeType = `image/${format}`;
2760
+ const dataUrl = tempCanvas.toDataURL(mimeType, quality);
2761
+ const imageData = dataUrl.split(',')[1]; // Remove data URL prefix
2762
+ return {
2763
+ imageData,
2764
+ width: targetWidth,
2765
+ height: targetHeight,
2766
+ format,
2767
+ timestamp: Date.now(),
2768
+ };
2769
+ }
2770
+ // =============================================================================
2771
+ // Duration Action Handling
2772
+ // =============================================================================
2773
+ applyDurationLerpState(action, t) {
2774
+ const { startState, targetState, params } = action;
2775
+ const deviceId = params.device;
2776
+ let newPosition;
2777
+ let newOrientation;
2778
+ if (startState.position && targetState.position) {
2779
+ newPosition = lerpVec3(startState.position, targetState.position, t);
2780
+ }
2781
+ if (startState.orientation && targetState.orientation) {
2782
+ newOrientation = slerpQuat(startState.orientation, targetState.orientation, t);
2783
+ }
2784
+ this.setDeviceTransform(deviceId, newPosition, newOrientation);
2785
+ }
2786
+ applyDurationFinalState(action) {
2787
+ const { targetState, params } = action;
2788
+ const deviceId = params.device;
2789
+ this.setDeviceTransform(deviceId, targetState.position, targetState.orientation);
2790
+ }
2791
+ getDurationResult(action) {
2792
+ const { params, elapsedMs } = action;
2793
+ const deviceId = params.device;
2794
+ const transform = this.getDeviceTransform(deviceId);
2795
+ return {
2796
+ device: deviceId,
2797
+ position: transform.position,
2798
+ orientation: transform.orientation,
2799
+ actualDuration: elapsedMs / 1000,
2800
+ };
2801
+ }
2802
+ /**
2803
+ * Activate capture mode for programmatic control.
2804
+ * Called when active methods are executed.
2805
+ */
2806
+ activateCaptureMode() {
2807
+ if (!this._isCaptured) {
2808
+ this._isCaptured = true;
2809
+ this.cancelReleaseTimer();
2810
+ this.device.controlMode = 'programmatic';
2811
+ }
2812
+ // Reset the release timer
2813
+ this.startReleaseTimer();
2814
+ }
2815
+ /**
2816
+ * Dispatch a method call.
2817
+ *
2818
+ * Immediate methods (queries, session management) execute synchronously.
2819
+ * State-modifying methods require an active session and are queued for frame-synchronized execution.
2820
+ *
2821
+ * @param method - The method name (e.g., 'get_transform', 'animate_to')
2822
+ * @param params - The method parameters
2823
+ * @returns Promise that resolves with the method result
2824
+ */
2825
+ async dispatch(method, params = {}) {
2826
+ var _a;
2827
+ // Normalize device identifier aliases (e.g. "right" -> "controller-right")
2828
+ if (typeof params.device === 'string') {
2829
+ params.device = resolveDeviceId(params.device);
2830
+ }
2831
+ // Immediate methods execute synchronously without queue
2832
+ if (RemoteControlInterface.IMMEDIATE_METHODS.has(method)) {
2833
+ // Active immediate methods trigger capture mode
2834
+ if (RemoteControlInterface.ACTIVE_IMMEDIATE_METHODS.has(method)) {
2835
+ this.activateCaptureMode();
2836
+ }
2837
+ return this.executeImmediateMethod(method, params);
2838
+ }
2839
+ // Methods that modify state require an active session
2840
+ if (RemoteControlInterface.SESSION_REQUIRED_METHODS.has(method)) {
2841
+ if (!this.device.activeSession) {
2842
+ throw new Error(`Cannot execute '${method}': No active XR session. ` +
2843
+ `Use 'get_session_status' to check session state, and 'accept_session' to start a session.`);
2844
+ }
2845
+ }
2846
+ // Handle animate_to specially - it's a duration action
2847
+ if (method === 'animate_to') {
2848
+ const animateParams = params;
2849
+ const currentTransform = this.getDeviceTransform(animateParams.device);
2850
+ const durationMs = ((_a = animateParams.duration) !== null && _a !== void 0 ? _a : 0.5) * 1000;
2851
+ const targetOrientation = animateParams.orientation
2852
+ ? normalizeOrientation(animateParams.orientation)
2853
+ : undefined;
2854
+ // Transform target position from XR-origin-relative to GlobalSpace
2855
+ const targetPosition = animateParams.position
2856
+ ? this.transformXROriginToGlobal(animateParams.position)
2857
+ : undefined;
2858
+ return this.enqueueDuration(method, params, durationMs, {
2859
+ position: animateParams.position
2860
+ ? currentTransform.position
2861
+ : undefined,
2862
+ orientation: targetOrientation
2863
+ ? currentTransform.orientation
2864
+ : undefined,
2865
+ }, {
2866
+ position: targetPosition,
2867
+ orientation: targetOrientation,
2868
+ });
2869
+ }
2870
+ // Handle select specially - it's a discrete action that enqueues multiple sub-actions
2871
+ if (method === 'select') {
2872
+ const selectParams = params;
2873
+ return this.executeSelectSequence(selectParams);
2874
+ }
2875
+ // All other methods are discrete actions that go through the queue
2876
+ return this.enqueueDiscrete(method, params);
2877
+ }
2878
+ /**
2879
+ * Execute an immediate method synchronously (not queued).
2880
+ * Used for queries and session management that must work outside XR frames.
2881
+ */
2882
+ executeImmediateMethod(method, params) {
2883
+ switch (method) {
2884
+ case 'get_session_status':
2885
+ return this.executeGetSessionStatus();
2886
+ case 'accept_session':
2887
+ return this.executeAcceptSession();
2888
+ case 'end_session':
2889
+ return this.executeEndSession();
2890
+ case 'get_transform':
2891
+ return this.executeGetTransform(params);
2892
+ case 'get_select_value':
2893
+ return this.executeGetSelectValue(params);
2894
+ case 'get_gamepad_state':
2895
+ return this.executeGetGamepadState(params);
2896
+ case 'get_device_state':
2897
+ return this.executeGetDeviceState();
2898
+ case 'capture_canvas':
2899
+ return this.executeCaptureCanvas(params);
2900
+ default:
2901
+ throw new Error(`Unknown immediate method: ${method}`);
2902
+ }
2903
+ }
2904
+ /**
2905
+ * Execute select action - this directly enqueues the three sub-actions without awaiting
2906
+ * The caller's promise resolves when all sub-actions complete
2907
+ */
2908
+ executeSelectSequence(params) {
2909
+ const { device: deviceId, duration = 0.15 } = params;
2910
+ // Validate device upfront to prevent sub-actions from failing in the frame loop
2911
+ this.getDeviceSelectValue(deviceId);
2912
+ return new Promise((resolve, reject) => {
2913
+ // Track completion of all three actions
2914
+ let actionsCompleted = 0;
2915
+ const totalActions = 3;
2916
+ const checkComplete = () => {
2917
+ actionsCompleted++;
2918
+ if (actionsCompleted === totalActions) {
2919
+ resolve({
2920
+ device: deviceId,
2921
+ duration,
2922
+ });
2923
+ }
2924
+ };
2925
+ // Enqueue: set value to 1
2926
+ const action1 = {
2927
+ type: 'discrete',
2928
+ id: this.generateActionId(),
2929
+ method: '_select_press',
2930
+ params: { device: deviceId },
2931
+ resolve: checkComplete,
2932
+ reject,
2933
+ };
2934
+ // Enqueue: wait for duration
2935
+ const action2 = {
2936
+ type: 'duration',
2937
+ id: this.generateActionId(),
2938
+ method: '_select_wait',
2939
+ params: { device: deviceId },
2940
+ durationMs: duration * 1000,
2941
+ elapsedMs: 0,
2942
+ startState: {},
2943
+ targetState: {},
2944
+ resolve: checkComplete,
2945
+ reject,
2946
+ };
2947
+ // Enqueue: set value to 0
2948
+ const action3 = {
2949
+ type: 'discrete',
2950
+ id: this.generateActionId(),
2951
+ method: '_select_release',
2952
+ params: { device: deviceId },
2953
+ resolve: checkComplete,
2954
+ reject,
2955
+ };
2956
+ this.commandQueue.push(action1, action2, action3);
2957
+ });
2958
+ }
2959
+ /**
2960
+ * Accept an offered XR session (async wrapper for proper session activation)
2961
+ */
2962
+ async acceptSession() {
2963
+ if (!this.device.sessionOffered) {
2964
+ throw new Error('No session has been offered');
2965
+ }
2966
+ this.device.grantOfferedSession();
2967
+ // Wait for session to become active
2968
+ await waitForCondition(() => !!this.device.activeSession, 5000);
2969
+ // Just return success - caller can use get_session_status for details
2970
+ return { success: true };
2971
+ }
2972
+ /**
2973
+ * Force release capture mode (for testing/cleanup)
2974
+ */
2975
+ forceRelease() {
2976
+ this.cancelReleaseTimer();
2977
+ this._isCaptured = false;
2978
+ this.device.controlMode = 'manual';
2979
+ // Clear pending actions
2980
+ for (const action of this.commandQueue) {
2981
+ action.reject(new Error('Capture released'));
2982
+ }
2983
+ this.commandQueue = [];
2984
+ // Reset any stuck select/trigger values
2985
+ for (const hand of ['left', 'right']) {
2986
+ const controller = this.device.controllers[hand];
2987
+ if (controller) {
2988
+ controller.updateButtonValue('trigger', 0);
2989
+ controller.updateButtonValue('squeeze', 0);
2990
+ }
2991
+ }
2992
+ }
2993
+ }
2994
+ // =============================================================================
2995
+ // Public API - Dispatch
2996
+ // =============================================================================
2997
+ /**
2998
+ * Set of methods that execute immediately (synchronously) without going through the queue.
2999
+ * These are queries and session management commands that need to work outside of XR frames.
3000
+ */
3001
+ RemoteControlInterface.IMMEDIATE_METHODS = new Set([
3002
+ // Session management - must work before/after XR session
3003
+ 'get_session_status',
3004
+ 'accept_session',
3005
+ 'end_session',
3006
+ // Pure queries - just read current state
3007
+ 'get_transform',
3008
+ 'get_select_value',
3009
+ 'get_gamepad_state',
3010
+ 'get_device_state',
3011
+ // Canvas capture - reads current canvas state
3012
+ 'capture_canvas',
3013
+ ]);
3014
+ /**
3015
+ * Set of immediate methods that are "active" - they modify state and should trigger capture mode.
3016
+ * Passive methods (queries) should NOT trigger capture mode.
3017
+ */
3018
+ RemoteControlInterface.ACTIVE_IMMEDIATE_METHODS = new Set([
3019
+ 'accept_session',
3020
+ 'end_session',
3021
+ ]);
3022
+ /**
3023
+ * Set of methods that require an active XR session.
3024
+ * These are state-modifying methods that are processed during frame updates.
3025
+ */
3026
+ RemoteControlInterface.SESSION_REQUIRED_METHODS = new Set([
3027
+ 'set_transform',
3028
+ 'look_at',
3029
+ 'animate_to',
3030
+ 'set_input_mode',
3031
+ 'set_connected',
3032
+ 'set_select_value',
3033
+ 'select',
3034
+ 'set_gamepad_state',
3035
+ 'set_device_state',
3036
+ ]);
3037
+
1541
3038
  /**
1542
3039
  * Copyright (c) Meta Platforms, Inc. and affiliates.
1543
3040
  *
@@ -1884,6 +3381,31 @@
1884
3381
  console.warn(`Current controller does not have button ${id}.`);
1885
3382
  }
1886
3383
  }
3384
+ /**
3385
+ * Set button value immediately (bypasses pending mechanism).
3386
+ * Use this for programmatic control where value should be readable immediately.
3387
+ */
3388
+ setButtonValueImmediate(id, value) {
3389
+ if (value > 1 || value < 0) {
3390
+ console.warn(`Out-of-range value ${value} provided for button ${id}.`);
3391
+ return;
3392
+ }
3393
+ const gamepadButton = this[P_TRACKED_INPUT].inputSource.gamepad[P_GAMEPAD].buttonsMap[id];
3394
+ if (gamepadButton) {
3395
+ if (gamepadButton[P_GAMEPAD].type === 'binary' &&
3396
+ value != 1 &&
3397
+ value != 0) {
3398
+ console.warn(`Non-binary value ${value} provided for binary button ${id}.`);
3399
+ return;
3400
+ }
3401
+ // Set both value and pendingValue for immediate effect
3402
+ gamepadButton[P_GAMEPAD].value = value;
3403
+ gamepadButton[P_GAMEPAD].pendingValue = value;
3404
+ }
3405
+ else {
3406
+ console.warn(`Current controller does not have button ${id}.`);
3407
+ }
3408
+ }
1887
3409
  updateButtonTouch(id, touched) {
1888
3410
  const gamepadButton = this[P_TRACKED_INPUT].inputSource.gamepad[P_GAMEPAD].buttonsMap[id];
1889
3411
  if (gamepadButton) {
@@ -1925,6 +3447,37 @@
1925
3447
  console.warn(`Current controller does not have ${id} axes.`);
1926
3448
  }
1927
3449
  }
3450
+ /**
3451
+ * Get the current value of a button by id
3452
+ */
3453
+ getButtonValue(id) {
3454
+ var _a;
3455
+ const gamepadButton = this[P_TRACKED_INPUT].inputSource.gamepad[P_GAMEPAD].buttonsMap[id];
3456
+ if (gamepadButton) {
3457
+ return (_a = gamepadButton[P_GAMEPAD].pendingValue) !== null && _a !== void 0 ? _a : gamepadButton.value;
3458
+ }
3459
+ return 0;
3460
+ }
3461
+ /**
3462
+ * Get the touched state of a button by id
3463
+ */
3464
+ getButtonTouched(id) {
3465
+ const gamepadButton = this[P_TRACKED_INPUT].inputSource.gamepad[P_GAMEPAD].buttonsMap[id];
3466
+ if (gamepadButton) {
3467
+ return gamepadButton.touched;
3468
+ }
3469
+ return false;
3470
+ }
3471
+ /**
3472
+ * Get the current axes values for a given id (e.g., 'thumbstick')
3473
+ */
3474
+ getAxes(id = 'thumbstick') {
3475
+ const axesById = this[P_TRACKED_INPUT].inputSource.gamepad[P_GAMEPAD].axesMap[id];
3476
+ if (axesById) {
3477
+ return { x: axesById.x, y: axesById.y };
3478
+ }
3479
+ return { x: 0, y: 0 };
3480
+ }
1928
3481
  }
1929
3482
 
1930
3483
  /**
@@ -2074,6 +3627,60 @@
2074
3627
  }
2075
3628
  }
2076
3629
 
3630
+ /**
3631
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3632
+ *
3633
+ * This source code is licensed under the MIT license found in the
3634
+ * LICENSE file in the root directory of this source tree.
3635
+ */
3636
+ class XRCPUDepthInformation {
3637
+ constructor(data, width, height, normDepthBufferFromNormView, rawValueToMeters, dataFormat) {
3638
+ this[P_DEPTH_INFO] = {
3639
+ data,
3640
+ width,
3641
+ height,
3642
+ normDepthBufferFromNormView,
3643
+ rawValueToMeters,
3644
+ dataFormat,
3645
+ };
3646
+ }
3647
+ get data() {
3648
+ return this[P_DEPTH_INFO].data;
3649
+ }
3650
+ get width() {
3651
+ return this[P_DEPTH_INFO].width;
3652
+ }
3653
+ get height() {
3654
+ return this[P_DEPTH_INFO].height;
3655
+ }
3656
+ get normDepthBufferFromNormView() {
3657
+ return this[P_DEPTH_INFO].normDepthBufferFromNormView;
3658
+ }
3659
+ get rawValueToMeters() {
3660
+ return this[P_DEPTH_INFO].rawValueToMeters;
3661
+ }
3662
+ getDepthInMeters(x, y) {
3663
+ const { width, height, rawValueToMeters, data, dataFormat } = this[P_DEPTH_INFO];
3664
+ if (x < 0 || x >= 1 || y < 0 || y >= 1) {
3665
+ throw new RangeError('Normalized coordinates must be in [0, 1) range.');
3666
+ }
3667
+ const col = Math.floor(x * width);
3668
+ const row = Math.floor(y * height);
3669
+ if (dataFormat === 'float32') {
3670
+ const floatView = new Float32Array(data);
3671
+ const index = row * width + col;
3672
+ return floatView[index] * rawValueToMeters;
3673
+ }
3674
+ else {
3675
+ // luminance-alpha: 16-bit unsigned int packed as two bytes
3676
+ const byteView = new Uint8Array(data);
3677
+ const index = (row * width + col) * 2;
3678
+ const rawValue = byteView[index] + byteView[index + 1] * 256;
3679
+ return rawValue * rawValueToMeters;
3680
+ }
3681
+ }
3682
+ }
3683
+
2077
3684
  /**
2078
3685
  * Copyright (c) Meta Platforms, Inc. and affiliates.
2079
3686
  *
@@ -2575,6 +4182,7 @@
2575
4182
  detectedMeshes: new XRMeshSet(),
2576
4183
  trackedAnchors: session[P_SESSION].frameTrackedAnchors,
2577
4184
  hitTestResultsMap: new Map(),
4185
+ depthDataMap: new Map(),
2578
4186
  };
2579
4187
  }
2580
4188
  get session() {
@@ -2710,6 +4318,16 @@
2710
4318
  return [...this[P_FRAME].hitTestResultsMap.get(hitTestSource)];
2711
4319
  }
2712
4320
  }
4321
+ getDepthInformation(view) {
4322
+ var _a;
4323
+ if (!this[P_FRAME].active) {
4324
+ throw new DOMException('XRFrame access outside the callback that produced it is invalid.', 'InvalidStateError');
4325
+ }
4326
+ if (!this[P_FRAME].session[P_SESSION].enabledFeatures.includes('depth-sensing')) {
4327
+ throw new DOMException('depth-sensing feature is not enabled on this session.', 'InvalidStateError');
4328
+ }
4329
+ return (_a = this[P_FRAME].depthDataMap.get(view[P_VIEW].eye)) !== null && _a !== void 0 ? _a : null;
4330
+ }
2713
4331
  }
2714
4332
 
2715
4333
  /**
@@ -2857,6 +4475,9 @@
2857
4475
  if (this[P_SESSION].enabledFeatures.includes('mesh-detection')) {
2858
4476
  this[P_SESSION].updateTrackedMeshes(frame);
2859
4477
  }
4478
+ if (this[P_SESSION].enabledFeatures.includes('depth-sensing')) {
4479
+ this[P_SESSION].computeDepthSensing(frame);
4480
+ }
2860
4481
  if (this[P_SESSION].enabledFeatures.includes('hit-test')) {
2861
4482
  this[P_SESSION].computeHitTestResults(frame);
2862
4483
  }
@@ -2982,6 +4603,37 @@
2982
4603
  frame[P_FRAME].detectedMeshes.add(xrMesh);
2983
4604
  });
2984
4605
  },
4606
+ depthSensingUsage: 'cpu-optimized',
4607
+ depthSensingDataFormat: 'float32',
4608
+ computeDepthSensing: (frame) => {
4609
+ const sem = this[P_SESSION].device[P_DEVICE].sem;
4610
+ if (!sem)
4611
+ return;
4612
+ const { depthNear, depthFar } = this[P_SESSION].renderState;
4613
+ const baseLayer = this[P_SESSION].renderState.baseLayer;
4614
+ if (!baseLayer)
4615
+ return;
4616
+ const canvas = baseLayer.context.canvas;
4617
+ // Use a reduced resolution for depth buffer (1/4 of canvas)
4618
+ const depthWidth = Math.max(1, Math.floor(canvas.width / 4));
4619
+ const depthHeight = Math.max(1, Math.floor(canvas.height / 4));
4620
+ const eyes = this[P_SESSION].mode === 'inline'
4621
+ ? [XREye.None]
4622
+ : [XREye.Left, XREye.Right];
4623
+ for (const eye of eyes) {
4624
+ const projectionMatrix = this[P_SESSION].getProjectionMatrix(eye);
4625
+ const viewSpace = this[P_SESSION].device.viewSpaces[eye];
4626
+ const viewGlobalMatrix = XRSpaceUtils.calculateGlobalOffsetMatrix(viewSpace);
4627
+ const viewMatrix = create$5();
4628
+ invert(viewMatrix, viewGlobalMatrix);
4629
+ const result = sem.computeDepthBuffer(viewMatrix, projectionMatrix, depthWidth, depthHeight, depthNear, depthFar);
4630
+ if (result) {
4631
+ const depthInfo = new XRCPUDepthInformation(result.data, result.width, result.height, new XRRigidTransform(), // identity: depth buffer is aligned with view
4632
+ result.rawValueToMeters, this[P_SESSION].depthSensingDataFormat);
4633
+ frame[P_FRAME].depthDataMap.set(eye, depthInfo);
4634
+ }
4635
+ }
4636
+ },
2985
4637
  hitTestSources: new Set(),
2986
4638
  computeHitTestResults: (frame) => {
2987
4639
  const sem = this[P_SESSION].device[P_DEVICE].sem;
@@ -3051,6 +4703,12 @@
3051
4703
  get interactionMode() {
3052
4704
  return this[P_SESSION].device[P_DEVICE].interactionMode;
3053
4705
  }
4706
+ get depthUsage() {
4707
+ return this[P_SESSION].depthSensingUsage;
4708
+ }
4709
+ get depthDataFormat() {
4710
+ return this[P_SESSION].depthSensingDataFormat;
4711
+ }
3054
4712
  updateRenderState(state = {}) {
3055
4713
  var _a, _b, _c, _d;
3056
4714
  if (this[P_SESSION].ended) {
@@ -4181,9 +5839,9 @@
4181
5839
  getTranslation(toPosition, toMatrix);
4182
5840
  getRotation(toQuaternion, toMatrix);
4183
5841
  getScaling(toScale, toMatrix);
4184
- lerp(interpolatedPosition, fromPosition, toPosition, alpha);
5842
+ lerp$1(interpolatedPosition, fromPosition, toPosition, alpha);
4185
5843
  slerp(interpolatedQuaternion, fromQuaternion, toQuaternion, alpha);
4186
- lerp(interpolatedScale, fromScale, toScale, alpha);
5844
+ lerp$1(interpolatedScale, fromScale, toScale, alpha);
4187
5845
  fromRotationTranslationScale(out, interpolatedQuaternion, interpolatedPosition, interpolatedScale);
4188
5846
  return out;
4189
5847
  };
@@ -4508,7 +6166,7 @@
4508
6166
  const f1q = fromValues(lastTransform[3], lastTransform[4], lastTransform[5], lastTransform[6]);
4509
6167
  const f2p = fromValues$2(nextTransform[0], nextTransform[1], nextTransform[2]);
4510
6168
  const f2q = fromValues(nextTransform[3], nextTransform[4], nextTransform[5], nextTransform[6]);
4511
- lerp(this[P_ACTION_PLAYER].vec3, f1p, f2p, alpha);
6169
+ lerp$1(this[P_ACTION_PLAYER].vec3, f1p, f2p, alpha);
4512
6170
  slerp(this[P_ACTION_PLAYER].quat, f1q, f2q, alpha);
4513
6171
  fromRotationTranslation(space[P_SPACE].offsetMatrix, this[P_ACTION_PLAYER].quat, this[P_ACTION_PLAYER].vec3);
4514
6172
  }
@@ -4533,7 +6191,7 @@
4533
6191
  }
4534
6192
  }
4535
6193
 
4536
- const VERSION = "2.1.1";
6194
+ const VERSION = "2.2.1";
4537
6195
 
4538
6196
  /**
4539
6197
  * Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -7840,6 +9498,16 @@ void main() {
7840
9498
  },
7841
9499
  onFrameStart: (frame) => {
7842
9500
  var _a;
9501
+ // Calculate delta time for remote control
9502
+ const now = performance.now();
9503
+ const deltaTimeMs = this[P_DEVICE].lastFrameTime > 0
9504
+ ? now - this[P_DEVICE].lastFrameTime
9505
+ : 16.67; // Default to ~60fps
9506
+ this[P_DEVICE].lastFrameTime = now;
9507
+ // Update remote control interface
9508
+ if (this[P_DEVICE].remote) {
9509
+ this[P_DEVICE].remote.update(deltaTimeMs);
9510
+ }
7843
9511
  if ((_a = this[P_DEVICE].actionPlayer) === null || _a === void 0 ? void 0 : _a.playing) {
7844
9512
  this[P_DEVICE].actionPlayer.playFrame();
7845
9513
  }
@@ -7873,7 +9541,17 @@ void main() {
7873
9541
  }
7874
9542
  this[P_DEVICE].updateViews();
7875
9543
  },
9544
+ // control mode for programmatic access
9545
+ controlMode: 'manual',
9546
+ controlModeListeners: new Set(),
9547
+ stateChangeListeners: new Set(),
9548
+ // remote control interface - initialized after this object
9549
+ remote: null,
9550
+ // frame timing for remote update
9551
+ lastFrameTime: 0,
7876
9552
  };
9553
+ // Initialize remote control interface
9554
+ this[P_DEVICE].remote = new RemoteControlInterface(this);
7877
9555
  this[P_DEVICE].updateViews();
7878
9556
  }
7879
9557
  installRuntime(options) {
@@ -8036,6 +9714,14 @@ void main() {
8036
9714
  }
8037
9715
  return;
8038
9716
  }
9717
+ /**
9718
+ * Get the app canvas when an XR session is active.
9719
+ * Returns undefined if no session is active or no canvas is available.
9720
+ */
9721
+ get appCanvas() {
9722
+ var _a;
9723
+ return (_a = this[P_DEVICE].canvasData) === null || _a === void 0 ? void 0 : _a.canvas;
9724
+ }
8039
9725
  get activeSession() {
8040
9726
  var _a;
8041
9727
  return (_a = this[P_DEVICE].xrSystem) === null || _a === void 0 ? void 0 : _a[P_SYSTEM].activeSession;
@@ -8101,6 +9787,65 @@ void main() {
8101
9787
  get sem() {
8102
9788
  return this[P_DEVICE].sem;
8103
9789
  }
9790
+ get remote() {
9791
+ return this[P_DEVICE].remote;
9792
+ }
9793
+ // =============================================================================
9794
+ // Control Mode API
9795
+ // =============================================================================
9796
+ /**
9797
+ * Get the current control mode
9798
+ * - 'manual': User controls device via DevUI (default)
9799
+ * - 'programmatic': External API controls device
9800
+ */
9801
+ get controlMode() {
9802
+ return this[P_DEVICE].controlMode;
9803
+ }
9804
+ /**
9805
+ * Set the control mode
9806
+ * Notifies all registered listeners of the change
9807
+ */
9808
+ set controlMode(mode) {
9809
+ if (mode !== 'manual' && mode !== 'programmatic') {
9810
+ console.warn('control mode can only be "manual" or "programmatic"');
9811
+ return;
9812
+ }
9813
+ const prevMode = this[P_DEVICE].controlMode;
9814
+ if (prevMode !== mode) {
9815
+ this[P_DEVICE].controlMode = mode;
9816
+ this[P_DEVICE].controlModeListeners.forEach((listener) => listener(mode));
9817
+ }
9818
+ }
9819
+ /**
9820
+ * Register a listener to be notified when control mode changes
9821
+ * @param listener - Callback function that receives the new mode
9822
+ * @returns Unsubscribe function to remove the listener
9823
+ */
9824
+ onControlModeChange(listener) {
9825
+ this[P_DEVICE].controlModeListeners.add(listener);
9826
+ return () => {
9827
+ this[P_DEVICE].controlModeListeners.delete(listener);
9828
+ };
9829
+ }
9830
+ /**
9831
+ * Register a listener to be notified when device state changes
9832
+ * Called after programmatic state modifications
9833
+ * @param listener - Callback function
9834
+ * @returns Unsubscribe function to remove the listener
9835
+ */
9836
+ onStateChange(listener) {
9837
+ this[P_DEVICE].stateChangeListeners.add(listener);
9838
+ return () => {
9839
+ this[P_DEVICE].stateChangeListeners.delete(listener);
9840
+ };
9841
+ }
9842
+ /**
9843
+ * Notify all state change listeners that device state has been modified
9844
+ * Should be called after programmatic state modifications
9845
+ */
9846
+ notifyStateChange() {
9847
+ this[P_DEVICE].stateChangeListeners.forEach((listener) => listener());
9848
+ }
8104
9849
  }
8105
9850
 
8106
9851
  /**
@@ -8541,6 +10286,7 @@ void main() {
8541
10286
  exports.P_ACTION_RECORDER = P_ACTION_RECORDER;
8542
10287
  exports.P_ANCHOR = P_ANCHOR;
8543
10288
  exports.P_CONTROLLER = P_CONTROLLER;
10289
+ exports.P_DEPTH_INFO = P_DEPTH_INFO;
8544
10290
  exports.P_DEVICE = P_DEVICE;
8545
10291
  exports.P_FRAME = P_FRAME;
8546
10292
  exports.P_GAMEPAD = P_GAMEPAD;
@@ -8564,8 +10310,10 @@ void main() {
8564
10310
  exports.P_VIEWER_POSE = P_VIEWER_POSE;
8565
10311
  exports.P_VIEWPORT = P_VIEWPORT;
8566
10312
  exports.P_WEBGL_LAYER = P_WEBGL_LAYER;
10313
+ exports.RemoteControlInterface = RemoteControlInterface;
8567
10314
  exports.XRAnchor = XRAnchor;
8568
10315
  exports.XRAnchorSet = XRAnchorSet;
10316
+ exports.XRCPUDepthInformation = XRCPUDepthInformation;
8569
10317
  exports.XRDevice = XRDevice;
8570
10318
  exports.XRFrame = XRFrame;
8571
10319
  exports.XRHand = XRHand;
@@ -8594,10 +10342,17 @@ void main() {
8594
10342
  exports.XRViewerPose = XRViewerPose;
8595
10343
  exports.XRViewport = XRViewport;
8596
10344
  exports.XRWebGLLayer = XRWebGLLayer;
10345
+ exports.directionTo = directionTo;
10346
+ exports.eulerToQuat = eulerToQuat;
10347
+ exports.lookRotation = lookRotation;
8597
10348
  exports.metaQuest2 = metaQuest2;
8598
10349
  exports.metaQuest3 = metaQuest3;
8599
10350
  exports.metaQuestPro = metaQuestPro;
8600
10351
  exports.oculusQuest1 = oculusQuest1;
10352
+ exports.quatToEuler = quatToEuler;
10353
+ exports.quatToObj = quatToObj;
10354
+ exports.vec3ToObj = vec3ToObj;
10355
+ exports.waitForCondition = waitForCondition;
8601
10356
 
8602
10357
  Object.defineProperty(exports, '__esModule', { value: true });
8603
10358