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