iwer 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/iwer.js +1606 -5
- package/build/iwer.min.js +12 -12
- package/build/iwer.module.js +1599 -6
- package/build/iwer.module.min.js +12 -12
- package/lib/device/XRController.d.ts +20 -0
- package/lib/device/XRController.d.ts.map +1 -1
- package/lib/device/XRController.js +56 -0
- package/lib/device/XRController.js.map +1 -1
- package/lib/device/XRDevice.d.ts +43 -1
- package/lib/device/XRDevice.d.ts.map +1 -1
- package/lib/device/XRDevice.js +88 -0
- package/lib/device/XRDevice.js.map +1 -1
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -0
- package/lib/index.js.map +1 -1
- package/lib/remote/RemoteControlInterface.d.ts +172 -0
- package/lib/remote/RemoteControlInterface.d.ts.map +1 -0
- package/lib/remote/RemoteControlInterface.js +1194 -0
- package/lib/remote/RemoteControlInterface.js.map +1 -0
- package/lib/remote/index.d.ts +9 -0
- package/lib/remote/index.d.ts.map +1 -0
- package/lib/remote/index.js +8 -0
- package/lib/remote/index.js.map +1 -0
- package/lib/remote/types.d.ts +348 -0
- package/lib/remote/types.d.ts.map +1 -0
- package/lib/remote/types.js +8 -0
- package/lib/remote/types.js.map +1 -0
- package/lib/types/state.d.ts +46 -0
- package/lib/types/state.d.ts.map +1 -0
- package/lib/types/state.js +8 -0
- package/lib/types/state.js.map +1 -0
- package/lib/utils/control-math.d.ts +64 -0
- package/lib/utils/control-math.d.ts.map +1 -0
- package/lib/utils/control-math.js +238 -0
- package/lib/utils/control-math.js.map +1 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +10 -5
- package/lib/layers/XRWebGLBinding.d.ts +0 -92
- package/lib/layers/XRWebGLBinding.d.ts.map +0 -1
- package/lib/layers/XRWebGLBinding.js +0 -186
- 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)
|
|
@@ -1538,6 +1559,1435 @@
|
|
|
1538
1559
|
}
|
|
1539
1560
|
}
|
|
1540
1561
|
|
|
1562
|
+
/**
|
|
1563
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
1564
|
+
*
|
|
1565
|
+
* This source code is licensed under the MIT license found in the
|
|
1566
|
+
* LICENSE file in the root directory of this source tree.
|
|
1567
|
+
*/
|
|
1568
|
+
/**
|
|
1569
|
+
* Convert a Vector3-like object to a plain Vec3 object
|
|
1570
|
+
*/
|
|
1571
|
+
function vec3ToObj(v) {
|
|
1572
|
+
return { x: v.x, y: v.y, z: v.z };
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Convert a Quaternion-like object to a plain Quat object
|
|
1576
|
+
*/
|
|
1577
|
+
function quatToObj(q) {
|
|
1578
|
+
return { x: q.x, y: q.y, z: q.z, w: q.w };
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Convert quaternion to euler angles (in degrees)
|
|
1582
|
+
* Uses YXZ order (yaw-pitch-roll) which is standard for XR:
|
|
1583
|
+
* - Yaw: rotation around Y axis (turning left/right)
|
|
1584
|
+
* - Pitch: rotation around X axis (looking up/down)
|
|
1585
|
+
* - Roll: rotation around Z axis (tilting head)
|
|
1586
|
+
*/
|
|
1587
|
+
function quatToEuler(q) {
|
|
1588
|
+
const { x, y, z, w } = q;
|
|
1589
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
1590
|
+
// YXZ order
|
|
1591
|
+
const sinp = Math.max(-1, Math.min(1, 2 * (w * x - y * z)));
|
|
1592
|
+
let pitch;
|
|
1593
|
+
if (Math.abs(sinp) >= 1) {
|
|
1594
|
+
pitch = (Math.sign(sinp) * Math.PI) / 2;
|
|
1595
|
+
}
|
|
1596
|
+
else {
|
|
1597
|
+
pitch = Math.asin(sinp);
|
|
1598
|
+
}
|
|
1599
|
+
const siny_cosp = 2 * (w * y + x * z);
|
|
1600
|
+
const cosy_cosp = 1 - 2 * (x * x + y * y);
|
|
1601
|
+
const yaw = Math.atan2(siny_cosp, cosy_cosp);
|
|
1602
|
+
const sinr_cosp = 2 * (w * z + x * y);
|
|
1603
|
+
const cosr_cosp = 1 - 2 * (x * x + z * z);
|
|
1604
|
+
const roll = Math.atan2(sinr_cosp, cosr_cosp);
|
|
1605
|
+
return {
|
|
1606
|
+
pitch: pitch * RAD_TO_DEG,
|
|
1607
|
+
yaw: yaw * RAD_TO_DEG,
|
|
1608
|
+
roll: roll * RAD_TO_DEG,
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Convert euler angles (in degrees) to quaternion
|
|
1613
|
+
* Uses YXZ order (yaw-pitch-roll) which is standard for XR:
|
|
1614
|
+
* - Yaw: rotation around Y axis (turning left/right)
|
|
1615
|
+
* - Pitch: rotation around X axis (looking up/down)
|
|
1616
|
+
* - Roll: rotation around Z axis (tilting head)
|
|
1617
|
+
* Missing angles default to 0.
|
|
1618
|
+
*/
|
|
1619
|
+
function eulerToQuat(euler) {
|
|
1620
|
+
var _a, _b, _c;
|
|
1621
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
1622
|
+
const pitch = ((_a = euler.pitch) !== null && _a !== void 0 ? _a : 0) * DEG_TO_RAD; // X-axis
|
|
1623
|
+
const yaw = ((_b = euler.yaw) !== null && _b !== void 0 ? _b : 0) * DEG_TO_RAD; // Y-axis
|
|
1624
|
+
const roll = ((_c = euler.roll) !== null && _c !== void 0 ? _c : 0) * DEG_TO_RAD; // Z-axis
|
|
1625
|
+
// Half angles
|
|
1626
|
+
const cx = Math.cos(pitch * 0.5);
|
|
1627
|
+
const sx = Math.sin(pitch * 0.5);
|
|
1628
|
+
const cy = Math.cos(yaw * 0.5);
|
|
1629
|
+
const sy = Math.sin(yaw * 0.5);
|
|
1630
|
+
const cz = Math.cos(roll * 0.5);
|
|
1631
|
+
const sz = Math.sin(roll * 0.5);
|
|
1632
|
+
// YXZ order: first yaw, then pitch, then roll
|
|
1633
|
+
return {
|
|
1634
|
+
w: cx * cy * cz + sx * sy * sz,
|
|
1635
|
+
x: sx * cy * cz + cx * sy * sz,
|
|
1636
|
+
y: cx * sy * cz - sx * cy * sz,
|
|
1637
|
+
z: cx * cy * sz - sx * sy * cz,
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* Calculate normalized direction vector from one point to another
|
|
1642
|
+
* Returns default forward direction (0, 0, -1) if points are coincident
|
|
1643
|
+
*/
|
|
1644
|
+
function directionTo(from, to) {
|
|
1645
|
+
const dx = to.x - from.x;
|
|
1646
|
+
const dy = to.y - from.y;
|
|
1647
|
+
const dz = to.z - from.z;
|
|
1648
|
+
const length = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
1649
|
+
if (length === 0) {
|
|
1650
|
+
return { x: 0, y: 0, z: -1 }; // Default forward (WebXR convention)
|
|
1651
|
+
}
|
|
1652
|
+
return {
|
|
1653
|
+
x: dx / length,
|
|
1654
|
+
y: dy / length,
|
|
1655
|
+
z: dz / length,
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Calculate gimbal-style look rotation (yaw + pitch only, roll = 0)
|
|
1660
|
+
* This keeps the camera/headset level while looking at a target.
|
|
1661
|
+
* @param direction - The direction to look towards
|
|
1662
|
+
* @returns Quaternion with only yaw and pitch, no roll
|
|
1663
|
+
*/
|
|
1664
|
+
function lookRotationGimbal(direction) {
|
|
1665
|
+
// Calculate horizontal distance
|
|
1666
|
+
const horizontalDist = Math.sqrt(direction.x * direction.x + direction.z * direction.z);
|
|
1667
|
+
// Calculate yaw: rotation around Y axis to face target horizontally
|
|
1668
|
+
// atan2(-z, -x) gives angle from negative Z axis (forward in WebXR)
|
|
1669
|
+
// We use -z, x to match WebXR's -Z forward convention
|
|
1670
|
+
let yaw = 0;
|
|
1671
|
+
if (horizontalDist > 0.0001) {
|
|
1672
|
+
yaw = Math.atan2(-direction.x, -direction.z);
|
|
1673
|
+
}
|
|
1674
|
+
// Calculate pitch: rotation around X axis to look up/down
|
|
1675
|
+
// Positive direction.y (target above) = positive pitch (look up)
|
|
1676
|
+
// Negative direction.y (target below) = negative pitch (look down)
|
|
1677
|
+
const pitch = Math.atan2(direction.y, horizontalDist);
|
|
1678
|
+
// Convert to degrees and create quaternion (roll = 0)
|
|
1679
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
1680
|
+
return eulerToQuat({
|
|
1681
|
+
pitch: pitch * RAD_TO_DEG,
|
|
1682
|
+
yaw: yaw * RAD_TO_DEG,
|
|
1683
|
+
roll: 0,
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Calculate quaternion that looks from origin towards a direction
|
|
1688
|
+
* @param direction - The direction to look towards (will be normalized)
|
|
1689
|
+
* @param up - The up vector (default: world up Y-axis)
|
|
1690
|
+
*/
|
|
1691
|
+
function lookRotation(direction, up = { x: 0, y: 1, z: 0 }) {
|
|
1692
|
+
// Normalize direction
|
|
1693
|
+
const dirLen = Math.sqrt(direction.x * direction.x +
|
|
1694
|
+
direction.y * direction.y +
|
|
1695
|
+
direction.z * direction.z);
|
|
1696
|
+
if (dirLen === 0) {
|
|
1697
|
+
return { x: 0, y: 0, z: 0, w: 1 }; // Identity quaternion
|
|
1698
|
+
}
|
|
1699
|
+
const forward = {
|
|
1700
|
+
x: direction.x / dirLen,
|
|
1701
|
+
y: direction.y / dirLen,
|
|
1702
|
+
z: direction.z / dirLen,
|
|
1703
|
+
};
|
|
1704
|
+
// Calculate right vector (cross product of forward and up, NOT up and forward)
|
|
1705
|
+
// forward × up gives correct right-hand orientation
|
|
1706
|
+
const right = {
|
|
1707
|
+
x: forward.y * up.z - forward.z * up.y,
|
|
1708
|
+
y: forward.z * up.x - forward.x * up.z,
|
|
1709
|
+
z: forward.x * up.y - forward.y * up.x,
|
|
1710
|
+
};
|
|
1711
|
+
const rightLen = Math.sqrt(right.x * right.x + right.y * right.y + right.z * right.z);
|
|
1712
|
+
if (rightLen === 0) {
|
|
1713
|
+
// Direction is parallel to up, choose a different up
|
|
1714
|
+
const altUp = { x: 1, y: 0, z: 0 };
|
|
1715
|
+
right.x = forward.y * altUp.z - forward.z * altUp.y;
|
|
1716
|
+
right.y = forward.z * altUp.x - forward.x * altUp.z;
|
|
1717
|
+
right.z = forward.x * altUp.y - forward.y * altUp.x;
|
|
1718
|
+
const altRightLen = Math.sqrt(right.x * right.x + right.y * right.y + right.z * right.z);
|
|
1719
|
+
right.x /= altRightLen;
|
|
1720
|
+
right.y /= altRightLen;
|
|
1721
|
+
right.z /= altRightLen;
|
|
1722
|
+
}
|
|
1723
|
+
else {
|
|
1724
|
+
right.x /= rightLen;
|
|
1725
|
+
right.y /= rightLen;
|
|
1726
|
+
right.z /= rightLen;
|
|
1727
|
+
}
|
|
1728
|
+
// Recalculate up (cross product of right and forward for proper orientation)
|
|
1729
|
+
const newUp = {
|
|
1730
|
+
x: right.y * forward.z - right.z * forward.y,
|
|
1731
|
+
y: right.z * forward.x - right.x * forward.z,
|
|
1732
|
+
z: right.x * forward.y - right.y * forward.x,
|
|
1733
|
+
};
|
|
1734
|
+
// Build rotation matrix and convert to quaternion
|
|
1735
|
+
// Matrix: [right, newUp, -forward] (column vectors)
|
|
1736
|
+
const m00 = right.x, m01 = newUp.x, m02 = -forward.x;
|
|
1737
|
+
const m10 = right.y, m11 = newUp.y, m12 = -forward.y;
|
|
1738
|
+
const m20 = right.z, m21 = newUp.z, m22 = -forward.z;
|
|
1739
|
+
const trace = m00 + m11 + m22;
|
|
1740
|
+
let qw, qx, qy, qz;
|
|
1741
|
+
if (trace > 0) {
|
|
1742
|
+
const s = 0.5 / Math.sqrt(trace + 1.0);
|
|
1743
|
+
qw = 0.25 / s;
|
|
1744
|
+
qx = (m21 - m12) * s;
|
|
1745
|
+
qy = (m02 - m20) * s;
|
|
1746
|
+
qz = (m10 - m01) * s;
|
|
1747
|
+
}
|
|
1748
|
+
else if (m00 > m11 && m00 > m22) {
|
|
1749
|
+
const s = 2.0 * Math.sqrt(1.0 + m00 - m11 - m22);
|
|
1750
|
+
qw = (m21 - m12) / s;
|
|
1751
|
+
qx = 0.25 * s;
|
|
1752
|
+
qy = (m01 + m10) / s;
|
|
1753
|
+
qz = (m02 + m20) / s;
|
|
1754
|
+
}
|
|
1755
|
+
else if (m11 > m22) {
|
|
1756
|
+
const s = 2.0 * Math.sqrt(1.0 + m11 - m00 - m22);
|
|
1757
|
+
qw = (m02 - m20) / s;
|
|
1758
|
+
qx = (m01 + m10) / s;
|
|
1759
|
+
qy = 0.25 * s;
|
|
1760
|
+
qz = (m12 + m21) / s;
|
|
1761
|
+
}
|
|
1762
|
+
else {
|
|
1763
|
+
const s = 2.0 * Math.sqrt(1.0 + m22 - m00 - m11);
|
|
1764
|
+
qw = (m10 - m01) / s;
|
|
1765
|
+
qx = (m02 + m20) / s;
|
|
1766
|
+
qy = (m12 + m21) / s;
|
|
1767
|
+
qz = 0.25 * s;
|
|
1768
|
+
}
|
|
1769
|
+
// Normalize the quaternion to ensure unit length
|
|
1770
|
+
const len = Math.sqrt(qx * qx + qy * qy + qz * qz + qw * qw);
|
|
1771
|
+
if (len > 0) {
|
|
1772
|
+
qx /= len;
|
|
1773
|
+
qy /= len;
|
|
1774
|
+
qz /= len;
|
|
1775
|
+
qw /= len;
|
|
1776
|
+
}
|
|
1777
|
+
return { x: qx, y: qy, z: qz, w: qw };
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Wait for a condition to become true, checking each animation frame
|
|
1781
|
+
*/
|
|
1782
|
+
function waitForCondition(condition, timeoutMs = 5000) {
|
|
1783
|
+
return new Promise((resolve, reject) => {
|
|
1784
|
+
const startTime = Date.now();
|
|
1785
|
+
const check = () => {
|
|
1786
|
+
if (condition()) {
|
|
1787
|
+
resolve();
|
|
1788
|
+
}
|
|
1789
|
+
else if (Date.now() - startTime > timeoutMs) {
|
|
1790
|
+
reject(new Error('Timeout waiting for condition'));
|
|
1791
|
+
}
|
|
1792
|
+
else {
|
|
1793
|
+
requestAnimationFrame(check);
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
check();
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/**
|
|
1801
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
1802
|
+
*
|
|
1803
|
+
* This source code is licensed under the MIT license found in the
|
|
1804
|
+
* LICENSE file in the root directory of this source tree.
|
|
1805
|
+
*/
|
|
1806
|
+
/**
|
|
1807
|
+
* Check if an orientation input is euler angles (has any of pitch, yaw, or roll)
|
|
1808
|
+
*/
|
|
1809
|
+
function isEulerRotation(orientation) {
|
|
1810
|
+
return 'pitch' in orientation || 'yaw' in orientation || 'roll' in orientation;
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* Normalize an orientation input to a quaternion
|
|
1814
|
+
*/
|
|
1815
|
+
function normalizeOrientation(orientation) {
|
|
1816
|
+
if (isEulerRotation(orientation)) {
|
|
1817
|
+
return eulerToQuat(orientation);
|
|
1818
|
+
}
|
|
1819
|
+
return orientation;
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Linear interpolation for numbers
|
|
1823
|
+
*/
|
|
1824
|
+
function lerp(a, b, t) {
|
|
1825
|
+
return a + (b - a) * t;
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Linear interpolation for Vec3
|
|
1829
|
+
*/
|
|
1830
|
+
function lerpVec3(a, b, t) {
|
|
1831
|
+
return {
|
|
1832
|
+
x: lerp(a.x, b.x, t),
|
|
1833
|
+
y: lerp(a.y, b.y, t),
|
|
1834
|
+
z: lerp(a.z, b.z, t),
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Spherical linear interpolation for quaternions
|
|
1839
|
+
*/
|
|
1840
|
+
function slerpQuat(a, b, t) {
|
|
1841
|
+
// Compute dot product
|
|
1842
|
+
let dot = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
|
|
1843
|
+
// If dot is negative, negate one quaternion to take shorter path
|
|
1844
|
+
let bx = b.x, by = b.y, bz = b.z, bw = b.w;
|
|
1845
|
+
if (dot < 0) {
|
|
1846
|
+
dot = -dot;
|
|
1847
|
+
bx = -bx;
|
|
1848
|
+
by = -by;
|
|
1849
|
+
bz = -bz;
|
|
1850
|
+
bw = -bw;
|
|
1851
|
+
}
|
|
1852
|
+
// If quaternions are very close, use linear interpolation
|
|
1853
|
+
if (dot > 0.9995) {
|
|
1854
|
+
const result = {
|
|
1855
|
+
x: lerp(a.x, bx, t),
|
|
1856
|
+
y: lerp(a.y, by, t),
|
|
1857
|
+
z: lerp(a.z, bz, t),
|
|
1858
|
+
w: lerp(a.w, bw, t),
|
|
1859
|
+
};
|
|
1860
|
+
// Normalize
|
|
1861
|
+
const len = Math.sqrt(result.x * result.x +
|
|
1862
|
+
result.y * result.y +
|
|
1863
|
+
result.z * result.z +
|
|
1864
|
+
result.w * result.w);
|
|
1865
|
+
return {
|
|
1866
|
+
x: result.x / len,
|
|
1867
|
+
y: result.y / len,
|
|
1868
|
+
z: result.z / len,
|
|
1869
|
+
w: result.w / len,
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
// Standard slerp
|
|
1873
|
+
const theta0 = Math.acos(dot);
|
|
1874
|
+
const theta = theta0 * t;
|
|
1875
|
+
const sinTheta = Math.sin(theta);
|
|
1876
|
+
const sinTheta0 = Math.sin(theta0);
|
|
1877
|
+
const s0 = Math.cos(theta) - (dot * sinTheta) / sinTheta0;
|
|
1878
|
+
const s1 = sinTheta / sinTheta0;
|
|
1879
|
+
return {
|
|
1880
|
+
x: s0 * a.x + s1 * bx,
|
|
1881
|
+
y: s0 * a.y + s1 * by,
|
|
1882
|
+
z: s0 * a.z + s1 * bz,
|
|
1883
|
+
w: s0 * a.w + s1 * bw,
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* RemoteControlInterface provides frame-synchronized programmatic control of an XRDevice.
|
|
1888
|
+
*
|
|
1889
|
+
* This class implements a command queue that processes actions during each frame update,
|
|
1890
|
+
* enabling smooth animations and coordinated control with DevUI.
|
|
1891
|
+
*
|
|
1892
|
+
* Key features:
|
|
1893
|
+
* - Frame-synchronized execution: Commands are queued and processed during frame update
|
|
1894
|
+
* - Duration-based actions: Smooth animations via lerp over multiple frames
|
|
1895
|
+
* - Automatic capture/release: Captures device on first command, releases 30s after queue empties
|
|
1896
|
+
* - Unified device identifiers: 'headset', 'controller-left', 'hand-right', etc.
|
|
1897
|
+
*
|
|
1898
|
+
* Usage:
|
|
1899
|
+
* ```typescript
|
|
1900
|
+
* import { XRDevice, metaQuest3 } from 'iwer';
|
|
1901
|
+
*
|
|
1902
|
+
* const device = new XRDevice(metaQuest3);
|
|
1903
|
+
* device.installRuntime();
|
|
1904
|
+
*
|
|
1905
|
+
* // Get transform
|
|
1906
|
+
* const result = await device.remote.dispatch('get_transform', { device: 'headset' });
|
|
1907
|
+
*
|
|
1908
|
+
* // Animate headset to new position over 1 second
|
|
1909
|
+
* await device.remote.dispatch('animate_to', {
|
|
1910
|
+
* device: 'headset',
|
|
1911
|
+
* position: { x: 0, y: 1.6, z: -1 },
|
|
1912
|
+
* duration: 1.0
|
|
1913
|
+
* });
|
|
1914
|
+
* ```
|
|
1915
|
+
*/
|
|
1916
|
+
class RemoteControlInterface {
|
|
1917
|
+
constructor(device) {
|
|
1918
|
+
this.commandQueue = [];
|
|
1919
|
+
this._isCaptured = false;
|
|
1920
|
+
this.releaseTimer = null;
|
|
1921
|
+
this.actionIdCounter = 0;
|
|
1922
|
+
/** Release timeout in milliseconds (default: 30000 = 30 seconds) */
|
|
1923
|
+
this.RELEASE_TIMEOUT_MS = 30000;
|
|
1924
|
+
this.device = device;
|
|
1925
|
+
}
|
|
1926
|
+
generateActionId() {
|
|
1927
|
+
return `action_${++this.actionIdCounter}`;
|
|
1928
|
+
}
|
|
1929
|
+
// =============================================================================
|
|
1930
|
+
// Public Properties
|
|
1931
|
+
// =============================================================================
|
|
1932
|
+
/**
|
|
1933
|
+
* Whether the device is currently captured for programmatic control.
|
|
1934
|
+
* When true, DevUI should go into passive mode (sync FROM device only).
|
|
1935
|
+
*/
|
|
1936
|
+
get isCaptured() {
|
|
1937
|
+
return this._isCaptured;
|
|
1938
|
+
}
|
|
1939
|
+
/**
|
|
1940
|
+
* Number of pending actions in the queue
|
|
1941
|
+
*/
|
|
1942
|
+
get queueLength() {
|
|
1943
|
+
return this.commandQueue.length;
|
|
1944
|
+
}
|
|
1945
|
+
// =============================================================================
|
|
1946
|
+
// Queue Management
|
|
1947
|
+
// =============================================================================
|
|
1948
|
+
/**
|
|
1949
|
+
* Enqueue a discrete action for processing
|
|
1950
|
+
*/
|
|
1951
|
+
enqueueDiscrete(method, params) {
|
|
1952
|
+
return new Promise((resolve, reject) => {
|
|
1953
|
+
const action = {
|
|
1954
|
+
type: 'discrete',
|
|
1955
|
+
id: this.generateActionId(),
|
|
1956
|
+
method,
|
|
1957
|
+
params,
|
|
1958
|
+
resolve,
|
|
1959
|
+
reject,
|
|
1960
|
+
};
|
|
1961
|
+
this.commandQueue.push(action);
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Enqueue a duration action for processing
|
|
1966
|
+
*/
|
|
1967
|
+
enqueueDuration(method, params, durationMs, startState, targetState) {
|
|
1968
|
+
return new Promise((resolve, reject) => {
|
|
1969
|
+
const action = {
|
|
1970
|
+
type: 'duration',
|
|
1971
|
+
id: this.generateActionId(),
|
|
1972
|
+
method,
|
|
1973
|
+
params,
|
|
1974
|
+
durationMs,
|
|
1975
|
+
elapsedMs: 0,
|
|
1976
|
+
startState,
|
|
1977
|
+
targetState,
|
|
1978
|
+
resolve,
|
|
1979
|
+
reject,
|
|
1980
|
+
};
|
|
1981
|
+
this.commandQueue.push(action);
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1985
|
+
* Update method called each frame by XRDevice.
|
|
1986
|
+
* Processes the command queue and handles duration-based animations.
|
|
1987
|
+
*
|
|
1988
|
+
* @param deltaTimeMs - Time since last frame in milliseconds
|
|
1989
|
+
*/
|
|
1990
|
+
update(deltaTimeMs) {
|
|
1991
|
+
if (this.commandQueue.length === 0) {
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
// Always cancel pending release while queue is active
|
|
1995
|
+
this.cancelReleaseTimer();
|
|
1996
|
+
// Activate capture mode
|
|
1997
|
+
if (!this._isCaptured) {
|
|
1998
|
+
this._isCaptured = true;
|
|
1999
|
+
this.device.controlMode = 'programmatic';
|
|
2000
|
+
}
|
|
2001
|
+
while (this.commandQueue.length > 0) {
|
|
2002
|
+
const action = this.commandQueue[0];
|
|
2003
|
+
if (action.type === 'discrete') {
|
|
2004
|
+
// Execute discrete action immediately
|
|
2005
|
+
try {
|
|
2006
|
+
const result = this.executeDiscreteAction(action);
|
|
2007
|
+
action.resolve(result);
|
|
2008
|
+
}
|
|
2009
|
+
catch (error) {
|
|
2010
|
+
action.reject(error);
|
|
2011
|
+
}
|
|
2012
|
+
this.commandQueue.shift();
|
|
2013
|
+
// Continue to next action
|
|
2014
|
+
}
|
|
2015
|
+
else {
|
|
2016
|
+
// Duration action - lerp by delta time
|
|
2017
|
+
action.elapsedMs += deltaTimeMs;
|
|
2018
|
+
if (action.elapsedMs >= action.durationMs) {
|
|
2019
|
+
// Complete - apply final state
|
|
2020
|
+
this.applyDurationFinalState(action);
|
|
2021
|
+
action.resolve(this.getDurationResult(action));
|
|
2022
|
+
this.commandQueue.shift();
|
|
2023
|
+
// Continue to next action
|
|
2024
|
+
}
|
|
2025
|
+
else {
|
|
2026
|
+
// In progress - lerp
|
|
2027
|
+
const t = action.elapsedMs / action.durationMs;
|
|
2028
|
+
this.applyDurationLerpState(action, t);
|
|
2029
|
+
// Stop processing - wait for next frame
|
|
2030
|
+
break;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
// Notify state change
|
|
2035
|
+
this.device.notifyStateChange();
|
|
2036
|
+
// Start release timer if queue is empty
|
|
2037
|
+
if (this.commandQueue.length === 0) {
|
|
2038
|
+
this.startReleaseTimer();
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
startReleaseTimer() {
|
|
2042
|
+
this.cancelReleaseTimer();
|
|
2043
|
+
this.releaseTimer = setTimeout(() => {
|
|
2044
|
+
this._isCaptured = false;
|
|
2045
|
+
this.device.controlMode = 'manual';
|
|
2046
|
+
this.releaseTimer = null;
|
|
2047
|
+
}, this.RELEASE_TIMEOUT_MS);
|
|
2048
|
+
}
|
|
2049
|
+
cancelReleaseTimer() {
|
|
2050
|
+
if (this.releaseTimer !== null) {
|
|
2051
|
+
clearTimeout(this.releaseTimer);
|
|
2052
|
+
this.releaseTimer = null;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
// =============================================================================
|
|
2056
|
+
// Device Resolution
|
|
2057
|
+
// =============================================================================
|
|
2058
|
+
/**
|
|
2059
|
+
* Get the transform (position, quaternion) for a device
|
|
2060
|
+
*/
|
|
2061
|
+
getDeviceTransform(deviceId) {
|
|
2062
|
+
switch (deviceId) {
|
|
2063
|
+
case 'headset':
|
|
2064
|
+
return {
|
|
2065
|
+
position: vec3ToObj(this.device.position),
|
|
2066
|
+
orientation: quatToObj(this.device.quaternion),
|
|
2067
|
+
};
|
|
2068
|
+
case 'controller-left': {
|
|
2069
|
+
const controller = this.device.controllers.left;
|
|
2070
|
+
if (!controller)
|
|
2071
|
+
throw new Error('Left controller not available');
|
|
2072
|
+
return {
|
|
2073
|
+
position: vec3ToObj(controller.position),
|
|
2074
|
+
orientation: quatToObj(controller.quaternion),
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
case 'controller-right': {
|
|
2078
|
+
const controller = this.device.controllers.right;
|
|
2079
|
+
if (!controller)
|
|
2080
|
+
throw new Error('Right controller not available');
|
|
2081
|
+
return {
|
|
2082
|
+
position: vec3ToObj(controller.position),
|
|
2083
|
+
orientation: quatToObj(controller.quaternion),
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
case 'hand-left': {
|
|
2087
|
+
const hand = this.device.hands.left;
|
|
2088
|
+
if (!hand)
|
|
2089
|
+
throw new Error('Left hand not available');
|
|
2090
|
+
return {
|
|
2091
|
+
position: vec3ToObj(hand.position),
|
|
2092
|
+
orientation: quatToObj(hand.quaternion),
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
case 'hand-right': {
|
|
2096
|
+
const hand = this.device.hands.right;
|
|
2097
|
+
if (!hand)
|
|
2098
|
+
throw new Error('Right hand not available');
|
|
2099
|
+
return {
|
|
2100
|
+
position: vec3ToObj(hand.position),
|
|
2101
|
+
orientation: quatToObj(hand.quaternion),
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
default:
|
|
2105
|
+
throw new Error(`Unknown device: ${deviceId}`);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Set the transform for a device
|
|
2110
|
+
*/
|
|
2111
|
+
setDeviceTransform(deviceId, position, orientation) {
|
|
2112
|
+
switch (deviceId) {
|
|
2113
|
+
case 'headset':
|
|
2114
|
+
if (position) {
|
|
2115
|
+
this.device.position.set(position.x, position.y, position.z);
|
|
2116
|
+
}
|
|
2117
|
+
if (orientation) {
|
|
2118
|
+
this.device.quaternion.set(orientation.x, orientation.y, orientation.z, orientation.w);
|
|
2119
|
+
}
|
|
2120
|
+
break;
|
|
2121
|
+
case 'controller-left': {
|
|
2122
|
+
const controller = this.device.controllers.left;
|
|
2123
|
+
if (!controller)
|
|
2124
|
+
throw new Error('Left controller not available');
|
|
2125
|
+
if (position) {
|
|
2126
|
+
controller.position.set(position.x, position.y, position.z);
|
|
2127
|
+
}
|
|
2128
|
+
if (orientation) {
|
|
2129
|
+
controller.quaternion.set(orientation.x, orientation.y, orientation.z, orientation.w);
|
|
2130
|
+
}
|
|
2131
|
+
break;
|
|
2132
|
+
}
|
|
2133
|
+
case 'controller-right': {
|
|
2134
|
+
const controller = this.device.controllers.right;
|
|
2135
|
+
if (!controller)
|
|
2136
|
+
throw new Error('Right controller not available');
|
|
2137
|
+
if (position) {
|
|
2138
|
+
controller.position.set(position.x, position.y, position.z);
|
|
2139
|
+
}
|
|
2140
|
+
if (orientation) {
|
|
2141
|
+
controller.quaternion.set(orientation.x, orientation.y, orientation.z, orientation.w);
|
|
2142
|
+
}
|
|
2143
|
+
break;
|
|
2144
|
+
}
|
|
2145
|
+
case 'hand-left': {
|
|
2146
|
+
const hand = this.device.hands.left;
|
|
2147
|
+
if (!hand)
|
|
2148
|
+
throw new Error('Left hand not available');
|
|
2149
|
+
if (position) {
|
|
2150
|
+
hand.position.set(position.x, position.y, position.z);
|
|
2151
|
+
}
|
|
2152
|
+
if (orientation) {
|
|
2153
|
+
hand.quaternion.set(orientation.x, orientation.y, orientation.z, orientation.w);
|
|
2154
|
+
}
|
|
2155
|
+
break;
|
|
2156
|
+
}
|
|
2157
|
+
case 'hand-right': {
|
|
2158
|
+
const hand = this.device.hands.right;
|
|
2159
|
+
if (!hand)
|
|
2160
|
+
throw new Error('Right hand not available');
|
|
2161
|
+
if (position) {
|
|
2162
|
+
hand.position.set(position.x, position.y, position.z);
|
|
2163
|
+
}
|
|
2164
|
+
if (orientation) {
|
|
2165
|
+
hand.quaternion.set(orientation.x, orientation.y, orientation.z, orientation.w);
|
|
2166
|
+
}
|
|
2167
|
+
break;
|
|
2168
|
+
}
|
|
2169
|
+
default:
|
|
2170
|
+
throw new Error(`Unknown device: ${deviceId}`);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
/**
|
|
2174
|
+
* Transform a position from XR-origin-relative coordinates to GlobalSpace.
|
|
2175
|
+
* The XR origin is defined by the first reference space requested by the app.
|
|
2176
|
+
* This is necessary because device positions are in GlobalSpace, but positions
|
|
2177
|
+
* from get_object_transform are relative to the XR origin.
|
|
2178
|
+
*/
|
|
2179
|
+
transformXROriginToGlobal(position) {
|
|
2180
|
+
var _a, _b;
|
|
2181
|
+
const session = this.device.activeSession;
|
|
2182
|
+
if (!session) {
|
|
2183
|
+
return position;
|
|
2184
|
+
}
|
|
2185
|
+
const refSpaces = (_a = session[P_SESSION]) === null || _a === void 0 ? void 0 : _a.referenceSpaces;
|
|
2186
|
+
if (!refSpaces || refSpaces.length === 0) {
|
|
2187
|
+
return position;
|
|
2188
|
+
}
|
|
2189
|
+
// Use the first reference space (primary one requested by app)
|
|
2190
|
+
const primaryRefSpace = refSpaces[0];
|
|
2191
|
+
const offsetMatrix = (_b = primaryRefSpace[P_SPACE]) === null || _b === void 0 ? void 0 : _b.offsetMatrix;
|
|
2192
|
+
if (!offsetMatrix) {
|
|
2193
|
+
return position;
|
|
2194
|
+
}
|
|
2195
|
+
// Transform position from XR-origin space to GlobalSpace
|
|
2196
|
+
const posVec = fromValues$2(position.x, position.y, position.z);
|
|
2197
|
+
transformMat4$1(posVec, posVec, offsetMatrix);
|
|
2198
|
+
return {
|
|
2199
|
+
x: posVec[0],
|
|
2200
|
+
y: posVec[1],
|
|
2201
|
+
z: posVec[2],
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* Get the select value for an input device (trigger for controller, pinch for hand)
|
|
2206
|
+
*/
|
|
2207
|
+
getDeviceSelectValue(deviceId) {
|
|
2208
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
2209
|
+
switch (deviceId) {
|
|
2210
|
+
case 'controller-left':
|
|
2211
|
+
return (_b = (_a = this.device.controllers.left) === null || _a === void 0 ? void 0 : _a.getButtonValue('trigger')) !== null && _b !== void 0 ? _b : 0;
|
|
2212
|
+
case 'controller-right':
|
|
2213
|
+
return (_d = (_c = this.device.controllers.right) === null || _c === void 0 ? void 0 : _c.getButtonValue('trigger')) !== null && _d !== void 0 ? _d : 0;
|
|
2214
|
+
case 'hand-left':
|
|
2215
|
+
return (_f = (_e = this.device.hands.left) === null || _e === void 0 ? void 0 : _e.pinchValue) !== null && _f !== void 0 ? _f : 0;
|
|
2216
|
+
case 'hand-right':
|
|
2217
|
+
return (_h = (_g = this.device.hands.right) === null || _g === void 0 ? void 0 : _g.pinchValue) !== null && _h !== void 0 ? _h : 0;
|
|
2218
|
+
default:
|
|
2219
|
+
throw new Error(`Unknown input device: ${deviceId}`);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
/**
|
|
2223
|
+
* Set the select value for an input device
|
|
2224
|
+
*/
|
|
2225
|
+
setDeviceSelectValue(deviceId, value) {
|
|
2226
|
+
var _a, _b, _c, _d;
|
|
2227
|
+
switch (deviceId) {
|
|
2228
|
+
case 'controller-left':
|
|
2229
|
+
(_a = this.device.controllers.left) === null || _a === void 0 ? void 0 : _a.updateButtonValue('trigger', value);
|
|
2230
|
+
break;
|
|
2231
|
+
case 'controller-right':
|
|
2232
|
+
(_b = this.device.controllers.right) === null || _b === void 0 ? void 0 : _b.updateButtonValue('trigger', value);
|
|
2233
|
+
break;
|
|
2234
|
+
case 'hand-left':
|
|
2235
|
+
(_c = this.device.hands.left) === null || _c === void 0 ? void 0 : _c.updatePinchValue(value);
|
|
2236
|
+
break;
|
|
2237
|
+
case 'hand-right':
|
|
2238
|
+
(_d = this.device.hands.right) === null || _d === void 0 ? void 0 : _d.updatePinchValue(value);
|
|
2239
|
+
break;
|
|
2240
|
+
default:
|
|
2241
|
+
throw new Error(`Unknown input device: ${deviceId}`);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
/**
|
|
2245
|
+
* Set connected state for an input device
|
|
2246
|
+
*/
|
|
2247
|
+
setDeviceConnected(deviceId, connected) {
|
|
2248
|
+
switch (deviceId) {
|
|
2249
|
+
case 'controller-left':
|
|
2250
|
+
if (this.device.controllers.left) {
|
|
2251
|
+
this.device.controllers.left.connected = connected;
|
|
2252
|
+
}
|
|
2253
|
+
break;
|
|
2254
|
+
case 'controller-right':
|
|
2255
|
+
if (this.device.controllers.right) {
|
|
2256
|
+
this.device.controllers.right.connected = connected;
|
|
2257
|
+
}
|
|
2258
|
+
break;
|
|
2259
|
+
case 'hand-left':
|
|
2260
|
+
if (this.device.hands.left) {
|
|
2261
|
+
this.device.hands.left.connected = connected;
|
|
2262
|
+
}
|
|
2263
|
+
break;
|
|
2264
|
+
case 'hand-right':
|
|
2265
|
+
if (this.device.hands.right) {
|
|
2266
|
+
this.device.hands.right.connected = connected;
|
|
2267
|
+
}
|
|
2268
|
+
break;
|
|
2269
|
+
default:
|
|
2270
|
+
throw new Error(`Unknown input device: ${deviceId}`);
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
// =============================================================================
|
|
2274
|
+
// Discrete Action Execution
|
|
2275
|
+
// =============================================================================
|
|
2276
|
+
executeDiscreteAction(action) {
|
|
2277
|
+
const { method, params } = action;
|
|
2278
|
+
switch (method) {
|
|
2279
|
+
// Session tools
|
|
2280
|
+
case 'get_session_status':
|
|
2281
|
+
return this.executeGetSessionStatus();
|
|
2282
|
+
case 'accept_session':
|
|
2283
|
+
return this.executeAcceptSession();
|
|
2284
|
+
case 'end_session':
|
|
2285
|
+
return this.executeEndSession();
|
|
2286
|
+
// Transform tools
|
|
2287
|
+
case 'get_transform':
|
|
2288
|
+
return this.executeGetTransform(params);
|
|
2289
|
+
case 'set_transform':
|
|
2290
|
+
return this.executeSetTransform(params);
|
|
2291
|
+
case 'look_at':
|
|
2292
|
+
return this.executeLookAt(params);
|
|
2293
|
+
// Input tools
|
|
2294
|
+
case 'set_input_mode':
|
|
2295
|
+
return this.executeSetInputMode(params);
|
|
2296
|
+
case 'set_connected':
|
|
2297
|
+
return this.executeSetConnected(params);
|
|
2298
|
+
case 'get_select_value':
|
|
2299
|
+
return this.executeGetSelectValue(params);
|
|
2300
|
+
case 'set_select_value':
|
|
2301
|
+
return this.executeSetSelectValue(params);
|
|
2302
|
+
// Gamepad tools
|
|
2303
|
+
case 'get_gamepad_state':
|
|
2304
|
+
return this.executeGetGamepadState(params);
|
|
2305
|
+
case 'set_gamepad_state':
|
|
2306
|
+
return this.executeSetGamepadState(params);
|
|
2307
|
+
// State tools
|
|
2308
|
+
case 'get_device_state':
|
|
2309
|
+
return this.executeGetDeviceState();
|
|
2310
|
+
case 'set_device_state':
|
|
2311
|
+
return this.executeSetDeviceState(params);
|
|
2312
|
+
case 'capture_canvas':
|
|
2313
|
+
return this.executeCaptureCanvas(params);
|
|
2314
|
+
// Internal select sequence actions
|
|
2315
|
+
case '_select_press': {
|
|
2316
|
+
const deviceId = params.device;
|
|
2317
|
+
this.setDeviceSelectValue(deviceId, 1);
|
|
2318
|
+
return undefined;
|
|
2319
|
+
}
|
|
2320
|
+
case '_select_release': {
|
|
2321
|
+
const deviceId = params.device;
|
|
2322
|
+
this.setDeviceSelectValue(deviceId, 0);
|
|
2323
|
+
return undefined;
|
|
2324
|
+
}
|
|
2325
|
+
default:
|
|
2326
|
+
throw new Error(`Unknown method: ${method}`);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
// =============================================================================
|
|
2330
|
+
// Session Tool Implementations
|
|
2331
|
+
// =============================================================================
|
|
2332
|
+
executeGetSessionStatus() {
|
|
2333
|
+
const session = this.device.activeSession;
|
|
2334
|
+
return {
|
|
2335
|
+
deviceName: this.device.name,
|
|
2336
|
+
isRuntimeInstalled: true,
|
|
2337
|
+
sessionActive: !!session,
|
|
2338
|
+
sessionOffered: this.device.sessionOffered,
|
|
2339
|
+
sessionMode: session ? session.mode : null,
|
|
2340
|
+
enabledFeatures: session
|
|
2341
|
+
? Array.from(session.enabledFeatures || [])
|
|
2342
|
+
: [],
|
|
2343
|
+
visibilityState: this.device.visibilityState,
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
executeAcceptSession() {
|
|
2347
|
+
if (!this.device.sessionOffered) {
|
|
2348
|
+
throw new Error('No session has been offered');
|
|
2349
|
+
}
|
|
2350
|
+
this.device.grantOfferedSession();
|
|
2351
|
+
// Session activation is async - caller should use get_session_status to poll
|
|
2352
|
+
return { success: true };
|
|
2353
|
+
}
|
|
2354
|
+
executeEndSession() {
|
|
2355
|
+
const session = this.device.activeSession;
|
|
2356
|
+
if (!session) {
|
|
2357
|
+
throw new Error('No active session');
|
|
2358
|
+
}
|
|
2359
|
+
session.end();
|
|
2360
|
+
return { success: true };
|
|
2361
|
+
}
|
|
2362
|
+
// =============================================================================
|
|
2363
|
+
// Transform Tool Implementations
|
|
2364
|
+
// =============================================================================
|
|
2365
|
+
executeGetTransform(params) {
|
|
2366
|
+
const { device: deviceId } = params;
|
|
2367
|
+
const transform = this.getDeviceTransform(deviceId);
|
|
2368
|
+
return {
|
|
2369
|
+
device: deviceId,
|
|
2370
|
+
position: transform.position,
|
|
2371
|
+
orientation: transform.orientation,
|
|
2372
|
+
euler: quatToEuler(transform.orientation),
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
executeSetTransform(params) {
|
|
2376
|
+
const { device: deviceId, position, orientation } = params;
|
|
2377
|
+
const targetOrientation = orientation
|
|
2378
|
+
? normalizeOrientation(orientation)
|
|
2379
|
+
: undefined;
|
|
2380
|
+
this.setDeviceTransform(deviceId, position, targetOrientation);
|
|
2381
|
+
const newTransform = this.getDeviceTransform(deviceId);
|
|
2382
|
+
return {
|
|
2383
|
+
device: deviceId,
|
|
2384
|
+
position: newTransform.position,
|
|
2385
|
+
orientation: newTransform.orientation,
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
executeLookAt(params) {
|
|
2389
|
+
const { device: deviceId, target, moveToDistance } = params;
|
|
2390
|
+
const currentTransform = this.getDeviceTransform(deviceId);
|
|
2391
|
+
// Transform target from XR-origin-relative to GlobalSpace
|
|
2392
|
+
const targetInGlobal = this.transformXROriginToGlobal(target);
|
|
2393
|
+
// Calculate direction to target
|
|
2394
|
+
const direction = directionTo(currentTransform.position, targetInGlobal);
|
|
2395
|
+
// Calculate look rotation
|
|
2396
|
+
// Use gimbal rotation for headset (keeps it level, no roll)
|
|
2397
|
+
// Use standard lookRotation for controllers/hands (can tilt freely)
|
|
2398
|
+
const lookQuat = deviceId === 'headset'
|
|
2399
|
+
? lookRotationGimbal(direction)
|
|
2400
|
+
: lookRotation(direction);
|
|
2401
|
+
// Optionally move to a specific distance from target
|
|
2402
|
+
let newPosition;
|
|
2403
|
+
if (moveToDistance !== undefined) {
|
|
2404
|
+
newPosition = {
|
|
2405
|
+
x: targetInGlobal.x - direction.x * moveToDistance,
|
|
2406
|
+
y: targetInGlobal.y - direction.y * moveToDistance,
|
|
2407
|
+
z: targetInGlobal.z - direction.z * moveToDistance,
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
this.setDeviceTransform(deviceId, newPosition, lookQuat);
|
|
2411
|
+
const newTransform = this.getDeviceTransform(deviceId);
|
|
2412
|
+
return {
|
|
2413
|
+
device: deviceId,
|
|
2414
|
+
position: newTransform.position,
|
|
2415
|
+
orientation: newTransform.orientation,
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
// =============================================================================
|
|
2419
|
+
// Input Tool Implementations
|
|
2420
|
+
// =============================================================================
|
|
2421
|
+
executeSetInputMode(params) {
|
|
2422
|
+
var _a, _b, _c, _d;
|
|
2423
|
+
const { mode } = params;
|
|
2424
|
+
this.device.primaryInputMode = mode;
|
|
2425
|
+
const activeDevices = [];
|
|
2426
|
+
if (mode === 'controller') {
|
|
2427
|
+
if ((_a = this.device.controllers.left) === null || _a === void 0 ? void 0 : _a.connected) {
|
|
2428
|
+
activeDevices.push('controller-left');
|
|
2429
|
+
}
|
|
2430
|
+
if ((_b = this.device.controllers.right) === null || _b === void 0 ? void 0 : _b.connected) {
|
|
2431
|
+
activeDevices.push('controller-right');
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
else {
|
|
2435
|
+
if ((_c = this.device.hands.left) === null || _c === void 0 ? void 0 : _c.connected) {
|
|
2436
|
+
activeDevices.push('hand-left');
|
|
2437
|
+
}
|
|
2438
|
+
if ((_d = this.device.hands.right) === null || _d === void 0 ? void 0 : _d.connected) {
|
|
2439
|
+
activeDevices.push('hand-right');
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
return { mode, activeDevices };
|
|
2443
|
+
}
|
|
2444
|
+
executeSetConnected(params) {
|
|
2445
|
+
const { device: deviceId, connected } = params;
|
|
2446
|
+
this.setDeviceConnected(deviceId, connected);
|
|
2447
|
+
return { device: deviceId, connected };
|
|
2448
|
+
}
|
|
2449
|
+
executeGetSelectValue(params) {
|
|
2450
|
+
const { device: deviceId } = params;
|
|
2451
|
+
const value = this.getDeviceSelectValue(deviceId);
|
|
2452
|
+
return { device: deviceId, value };
|
|
2453
|
+
}
|
|
2454
|
+
executeSetSelectValue(params) {
|
|
2455
|
+
const { device: deviceId, value } = params;
|
|
2456
|
+
this.setDeviceSelectValue(deviceId, value);
|
|
2457
|
+
return { device: deviceId, value };
|
|
2458
|
+
}
|
|
2459
|
+
// =============================================================================
|
|
2460
|
+
// Gamepad Tool Implementations
|
|
2461
|
+
// =============================================================================
|
|
2462
|
+
executeGetGamepadState(params) {
|
|
2463
|
+
const { device: deviceId } = params;
|
|
2464
|
+
const hand = deviceId === 'controller-left' ? 'left' : 'right';
|
|
2465
|
+
const controller = this.device.controllers[hand];
|
|
2466
|
+
if (!controller) {
|
|
2467
|
+
throw new Error(`Controller ${hand} not available`);
|
|
2468
|
+
}
|
|
2469
|
+
// Button layout for Meta Quest Touch Plus controllers
|
|
2470
|
+
// Use hand-conditional internal names for lookup
|
|
2471
|
+
const buttonInternalNames = [
|
|
2472
|
+
'trigger',
|
|
2473
|
+
'squeeze',
|
|
2474
|
+
'thumbstick',
|
|
2475
|
+
hand === 'left' ? 'x-button' : 'a-button',
|
|
2476
|
+
hand === 'left' ? 'y-button' : 'b-button',
|
|
2477
|
+
'thumbrest',
|
|
2478
|
+
];
|
|
2479
|
+
const buttons = buttonInternalNames.map((name, index) => ({
|
|
2480
|
+
index,
|
|
2481
|
+
name: name
|
|
2482
|
+
.replace('x-button', 'x')
|
|
2483
|
+
.replace('y-button', 'y')
|
|
2484
|
+
.replace('a-button', 'a')
|
|
2485
|
+
.replace('b-button', 'b'),
|
|
2486
|
+
value: controller.getButtonValue(name),
|
|
2487
|
+
touched: controller.getButtonTouched(name),
|
|
2488
|
+
pressed: controller.getButtonValue(name) > 0.5,
|
|
2489
|
+
}));
|
|
2490
|
+
const axesData = controller.getAxes();
|
|
2491
|
+
const axes = [
|
|
2492
|
+
{ index: 0, name: 'thumbstick-x', value: axesData.x },
|
|
2493
|
+
{ index: 1, name: 'thumbstick-y', value: axesData.y },
|
|
2494
|
+
];
|
|
2495
|
+
return {
|
|
2496
|
+
device: deviceId,
|
|
2497
|
+
connected: controller.connected,
|
|
2498
|
+
buttons,
|
|
2499
|
+
axes,
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
executeSetGamepadState(params) {
|
|
2503
|
+
const { device: deviceId, buttons, axes } = params;
|
|
2504
|
+
const hand = deviceId === 'controller-left' ? 'left' : 'right';
|
|
2505
|
+
const controller = this.device.controllers[hand];
|
|
2506
|
+
if (!controller) {
|
|
2507
|
+
throw new Error(`Controller ${hand} not available`);
|
|
2508
|
+
}
|
|
2509
|
+
let buttonsSet = 0;
|
|
2510
|
+
let axesSet = 0;
|
|
2511
|
+
// Button index to name mapping
|
|
2512
|
+
const buttonIndexToName = [
|
|
2513
|
+
'trigger',
|
|
2514
|
+
'squeeze',
|
|
2515
|
+
'thumbstick',
|
|
2516
|
+
hand === 'left' ? 'x-button' : 'a-button',
|
|
2517
|
+
hand === 'left' ? 'y-button' : 'b-button',
|
|
2518
|
+
'thumbrest',
|
|
2519
|
+
];
|
|
2520
|
+
if (buttons) {
|
|
2521
|
+
for (const btn of buttons) {
|
|
2522
|
+
const buttonName = buttonIndexToName[btn.index];
|
|
2523
|
+
if (buttonName) {
|
|
2524
|
+
// Use updateButtonValue for proper event triggering
|
|
2525
|
+
controller.updateButtonValue(buttonName, btn.value);
|
|
2526
|
+
if (btn.touched !== undefined) {
|
|
2527
|
+
controller.updateButtonTouch(buttonName, btn.touched);
|
|
2528
|
+
}
|
|
2529
|
+
buttonsSet++;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
if (axes) {
|
|
2534
|
+
let xValue;
|
|
2535
|
+
let yValue;
|
|
2536
|
+
for (const axis of axes) {
|
|
2537
|
+
if (axis.index === 0) {
|
|
2538
|
+
xValue = axis.value;
|
|
2539
|
+
axesSet++;
|
|
2540
|
+
}
|
|
2541
|
+
else if (axis.index === 1) {
|
|
2542
|
+
yValue = axis.value;
|
|
2543
|
+
axesSet++;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
if (xValue !== undefined || yValue !== undefined) {
|
|
2547
|
+
const currentAxes = controller.getAxes();
|
|
2548
|
+
controller.updateAxes('thumbstick', xValue !== null && xValue !== void 0 ? xValue : currentAxes.x, yValue !== null && yValue !== void 0 ? yValue : currentAxes.y);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
return { device: deviceId, buttonsSet, axesSet };
|
|
2552
|
+
}
|
|
2553
|
+
// =============================================================================
|
|
2554
|
+
// State Tool Implementations
|
|
2555
|
+
// =============================================================================
|
|
2556
|
+
executeGetDeviceState() {
|
|
2557
|
+
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;
|
|
2558
|
+
return {
|
|
2559
|
+
headset: {
|
|
2560
|
+
position: vec3ToObj(this.device.position),
|
|
2561
|
+
orientation: quatToObj(this.device.quaternion),
|
|
2562
|
+
},
|
|
2563
|
+
inputMode: this.device.primaryInputMode,
|
|
2564
|
+
controllers: {
|
|
2565
|
+
left: {
|
|
2566
|
+
connected: (_b = (_a = this.device.controllers.left) === null || _a === void 0 ? void 0 : _a.connected) !== null && _b !== void 0 ? _b : false,
|
|
2567
|
+
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 }),
|
|
2568
|
+
orientation: quatToObj((_f = (_e = this.device.controllers.left) === null || _e === void 0 ? void 0 : _e.quaternion) !== null && _f !== void 0 ? _f : {
|
|
2569
|
+
x: 0,
|
|
2570
|
+
y: 0,
|
|
2571
|
+
z: 0,
|
|
2572
|
+
w: 1,
|
|
2573
|
+
}),
|
|
2574
|
+
},
|
|
2575
|
+
right: {
|
|
2576
|
+
connected: (_h = (_g = this.device.controllers.right) === null || _g === void 0 ? void 0 : _g.connected) !== null && _h !== void 0 ? _h : false,
|
|
2577
|
+
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 }),
|
|
2578
|
+
orientation: quatToObj((_m = (_l = this.device.controllers.right) === null || _l === void 0 ? void 0 : _l.quaternion) !== null && _m !== void 0 ? _m : {
|
|
2579
|
+
x: 0,
|
|
2580
|
+
y: 0,
|
|
2581
|
+
z: 0,
|
|
2582
|
+
w: 1,
|
|
2583
|
+
}),
|
|
2584
|
+
},
|
|
2585
|
+
},
|
|
2586
|
+
hands: {
|
|
2587
|
+
left: {
|
|
2588
|
+
connected: (_p = (_o = this.device.hands.left) === null || _o === void 0 ? void 0 : _o.connected) !== null && _p !== void 0 ? _p : false,
|
|
2589
|
+
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 }),
|
|
2590
|
+
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 }),
|
|
2591
|
+
},
|
|
2592
|
+
right: {
|
|
2593
|
+
connected: (_v = (_u = this.device.hands.right) === null || _u === void 0 ? void 0 : _u.connected) !== null && _v !== void 0 ? _v : false,
|
|
2594
|
+
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 }),
|
|
2595
|
+
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 }),
|
|
2596
|
+
},
|
|
2597
|
+
},
|
|
2598
|
+
stereoEnabled: this.device.stereoEnabled,
|
|
2599
|
+
fov: this.device.fovy * (180 / Math.PI), // Convert to degrees
|
|
2600
|
+
};
|
|
2601
|
+
}
|
|
2602
|
+
executeSetDeviceState(params) {
|
|
2603
|
+
const { state } = params;
|
|
2604
|
+
if (!state) {
|
|
2605
|
+
// Reset to initial state
|
|
2606
|
+
this.device.position.set(0, 1.6, 0);
|
|
2607
|
+
this.device.quaternion.set(0, 0, 0, 1);
|
|
2608
|
+
this.device.primaryInputMode = 'controller';
|
|
2609
|
+
this.device.stereoEnabled = false;
|
|
2610
|
+
// Reset controllers and hands to default positions
|
|
2611
|
+
if (this.device.controllers.left) {
|
|
2612
|
+
this.device.controllers.left.position.set(-0.2, 1.4, -0.3);
|
|
2613
|
+
this.device.controllers.left.quaternion.set(0, 0, 0, 1);
|
|
2614
|
+
this.device.controllers.left.connected = true;
|
|
2615
|
+
}
|
|
2616
|
+
if (this.device.controllers.right) {
|
|
2617
|
+
this.device.controllers.right.position.set(0.2, 1.4, -0.3);
|
|
2618
|
+
this.device.controllers.right.quaternion.set(0, 0, 0, 1);
|
|
2619
|
+
this.device.controllers.right.connected = true;
|
|
2620
|
+
}
|
|
2621
|
+
if (this.device.hands.left) {
|
|
2622
|
+
this.device.hands.left.position.set(-0.15, 1.3, -0.4);
|
|
2623
|
+
this.device.hands.left.quaternion.set(0, 0, 0, 1);
|
|
2624
|
+
this.device.hands.left.connected = true;
|
|
2625
|
+
}
|
|
2626
|
+
if (this.device.hands.right) {
|
|
2627
|
+
this.device.hands.right.position.set(0.15, 1.3, -0.4);
|
|
2628
|
+
this.device.hands.right.quaternion.set(0, 0, 0, 1);
|
|
2629
|
+
this.device.hands.right.connected = true;
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
else {
|
|
2633
|
+
// Apply partial state
|
|
2634
|
+
if (state.headset) {
|
|
2635
|
+
if (state.headset.position) {
|
|
2636
|
+
this.device.position.set(state.headset.position.x, state.headset.position.y, state.headset.position.z);
|
|
2637
|
+
}
|
|
2638
|
+
if (state.headset.orientation) {
|
|
2639
|
+
this.device.quaternion.set(state.headset.orientation.x, state.headset.orientation.y, state.headset.orientation.z, state.headset.orientation.w);
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
if (state.inputMode !== undefined) {
|
|
2643
|
+
this.device.primaryInputMode = state.inputMode;
|
|
2644
|
+
}
|
|
2645
|
+
if (state.stereoEnabled !== undefined) {
|
|
2646
|
+
this.device.stereoEnabled = state.stereoEnabled;
|
|
2647
|
+
}
|
|
2648
|
+
if (state.fov !== undefined) {
|
|
2649
|
+
this.device.fovy = state.fov * (Math.PI / 180); // Convert to radians
|
|
2650
|
+
}
|
|
2651
|
+
if (state.controllers) {
|
|
2652
|
+
this.applyInputState('controller-left', state.controllers.left);
|
|
2653
|
+
this.applyInputState('controller-right', state.controllers.right);
|
|
2654
|
+
}
|
|
2655
|
+
if (state.hands) {
|
|
2656
|
+
this.applyInputState('hand-left', state.hands.left);
|
|
2657
|
+
this.applyInputState('hand-right', state.hands.right);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
return { state: this.executeGetDeviceState() };
|
|
2661
|
+
}
|
|
2662
|
+
applyInputState(deviceId, state) {
|
|
2663
|
+
if (!state)
|
|
2664
|
+
return;
|
|
2665
|
+
if (state.connected !== undefined) {
|
|
2666
|
+
this.setDeviceConnected(deviceId, state.connected);
|
|
2667
|
+
}
|
|
2668
|
+
if (state.position || state.orientation) {
|
|
2669
|
+
this.setDeviceTransform(deviceId, state.position, state.orientation);
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
executeCaptureCanvas(params) {
|
|
2673
|
+
const { maxWidth = 800, format = 'png', quality = 0.92 } = params;
|
|
2674
|
+
// Get the app canvas - try device first, then fallback to DOM query
|
|
2675
|
+
let canvas = this.device.appCanvas;
|
|
2676
|
+
if (!canvas) {
|
|
2677
|
+
// No active session - try to find the canvas in the DOM
|
|
2678
|
+
// Before XR session, only the app's canvas is in the DOM
|
|
2679
|
+
// (IWER's canvases are not added until session starts)
|
|
2680
|
+
const canvases = document.querySelectorAll('canvas');
|
|
2681
|
+
if (canvases.length === 1) {
|
|
2682
|
+
canvas = canvases[0];
|
|
2683
|
+
}
|
|
2684
|
+
else if (canvases.length > 1) {
|
|
2685
|
+
// Multiple canvases - try to find the most likely app canvas
|
|
2686
|
+
// Prefer the largest visible canvas
|
|
2687
|
+
let bestCanvas = null;
|
|
2688
|
+
let bestArea = 0;
|
|
2689
|
+
canvases.forEach((c) => {
|
|
2690
|
+
const rect = c.getBoundingClientRect();
|
|
2691
|
+
const area = rect.width * rect.height;
|
|
2692
|
+
if (area > bestArea && rect.width > 0 && rect.height > 0) {
|
|
2693
|
+
bestArea = area;
|
|
2694
|
+
bestCanvas = c;
|
|
2695
|
+
}
|
|
2696
|
+
});
|
|
2697
|
+
canvas = bestCanvas;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
if (!canvas) {
|
|
2701
|
+
throw new Error('No canvas available. Either start an XR session or ensure an app canvas is in the DOM.');
|
|
2702
|
+
}
|
|
2703
|
+
// Create a temporary canvas for scaling
|
|
2704
|
+
const tempCanvas = document.createElement('canvas');
|
|
2705
|
+
const ctx = tempCanvas.getContext('2d');
|
|
2706
|
+
if (!ctx) {
|
|
2707
|
+
throw new Error('Failed to create canvas context');
|
|
2708
|
+
}
|
|
2709
|
+
// Calculate scaled dimensions
|
|
2710
|
+
const aspectRatio = canvas.height / canvas.width;
|
|
2711
|
+
const targetWidth = Math.min(canvas.width, maxWidth);
|
|
2712
|
+
const targetHeight = Math.round(targetWidth * aspectRatio);
|
|
2713
|
+
tempCanvas.width = targetWidth;
|
|
2714
|
+
tempCanvas.height = targetHeight;
|
|
2715
|
+
// Draw scaled image
|
|
2716
|
+
ctx.drawImage(canvas, 0, 0, targetWidth, targetHeight);
|
|
2717
|
+
// Convert to base64
|
|
2718
|
+
const mimeType = `image/${format}`;
|
|
2719
|
+
const dataUrl = tempCanvas.toDataURL(mimeType, quality);
|
|
2720
|
+
const imageData = dataUrl.split(',')[1]; // Remove data URL prefix
|
|
2721
|
+
return {
|
|
2722
|
+
imageData,
|
|
2723
|
+
width: targetWidth,
|
|
2724
|
+
height: targetHeight,
|
|
2725
|
+
format,
|
|
2726
|
+
timestamp: Date.now(),
|
|
2727
|
+
};
|
|
2728
|
+
}
|
|
2729
|
+
// =============================================================================
|
|
2730
|
+
// Duration Action Handling
|
|
2731
|
+
// =============================================================================
|
|
2732
|
+
applyDurationLerpState(action, t) {
|
|
2733
|
+
const { startState, targetState, params } = action;
|
|
2734
|
+
const deviceId = params.device;
|
|
2735
|
+
let newPosition;
|
|
2736
|
+
let newOrientation;
|
|
2737
|
+
if (startState.position && targetState.position) {
|
|
2738
|
+
newPosition = lerpVec3(startState.position, targetState.position, t);
|
|
2739
|
+
}
|
|
2740
|
+
if (startState.orientation && targetState.orientation) {
|
|
2741
|
+
newOrientation = slerpQuat(startState.orientation, targetState.orientation, t);
|
|
2742
|
+
}
|
|
2743
|
+
this.setDeviceTransform(deviceId, newPosition, newOrientation);
|
|
2744
|
+
}
|
|
2745
|
+
applyDurationFinalState(action) {
|
|
2746
|
+
const { targetState, params } = action;
|
|
2747
|
+
const deviceId = params.device;
|
|
2748
|
+
this.setDeviceTransform(deviceId, targetState.position, targetState.orientation);
|
|
2749
|
+
}
|
|
2750
|
+
getDurationResult(action) {
|
|
2751
|
+
const { params, elapsedMs } = action;
|
|
2752
|
+
const deviceId = params.device;
|
|
2753
|
+
const transform = this.getDeviceTransform(deviceId);
|
|
2754
|
+
return {
|
|
2755
|
+
device: deviceId,
|
|
2756
|
+
position: transform.position,
|
|
2757
|
+
orientation: transform.orientation,
|
|
2758
|
+
actualDuration: elapsedMs / 1000,
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
/**
|
|
2762
|
+
* Activate capture mode for programmatic control.
|
|
2763
|
+
* Called when active methods are executed.
|
|
2764
|
+
*/
|
|
2765
|
+
activateCaptureMode() {
|
|
2766
|
+
if (!this._isCaptured) {
|
|
2767
|
+
this._isCaptured = true;
|
|
2768
|
+
this.cancelReleaseTimer();
|
|
2769
|
+
this.device.controlMode = 'programmatic';
|
|
2770
|
+
}
|
|
2771
|
+
// Reset the release timer
|
|
2772
|
+
this.startReleaseTimer();
|
|
2773
|
+
}
|
|
2774
|
+
/**
|
|
2775
|
+
* Dispatch a method call.
|
|
2776
|
+
*
|
|
2777
|
+
* Immediate methods (queries, session management) execute synchronously.
|
|
2778
|
+
* State-modifying methods require an active session and are queued for frame-synchronized execution.
|
|
2779
|
+
*
|
|
2780
|
+
* @param method - The method name (e.g., 'get_transform', 'animate_to')
|
|
2781
|
+
* @param params - The method parameters
|
|
2782
|
+
* @returns Promise that resolves with the method result
|
|
2783
|
+
*/
|
|
2784
|
+
async dispatch(method, params = {}) {
|
|
2785
|
+
var _a;
|
|
2786
|
+
// Immediate methods execute synchronously without queue
|
|
2787
|
+
if (RemoteControlInterface.IMMEDIATE_METHODS.has(method)) {
|
|
2788
|
+
// Active immediate methods trigger capture mode
|
|
2789
|
+
if (RemoteControlInterface.ACTIVE_IMMEDIATE_METHODS.has(method)) {
|
|
2790
|
+
this.activateCaptureMode();
|
|
2791
|
+
}
|
|
2792
|
+
return this.executeImmediateMethod(method, params);
|
|
2793
|
+
}
|
|
2794
|
+
// Methods that modify state require an active session
|
|
2795
|
+
if (RemoteControlInterface.SESSION_REQUIRED_METHODS.has(method)) {
|
|
2796
|
+
if (!this.device.activeSession) {
|
|
2797
|
+
throw new Error(`Cannot execute '${method}': No active XR session. ` +
|
|
2798
|
+
`Use 'get_session_status' to check session state, and 'accept_session' to start a session.`);
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
// Handle animate_to specially - it's a duration action
|
|
2802
|
+
if (method === 'animate_to') {
|
|
2803
|
+
const animateParams = params;
|
|
2804
|
+
const currentTransform = this.getDeviceTransform(animateParams.device);
|
|
2805
|
+
const durationMs = ((_a = animateParams.duration) !== null && _a !== void 0 ? _a : 0.5) * 1000;
|
|
2806
|
+
const targetOrientation = animateParams.orientation
|
|
2807
|
+
? normalizeOrientation(animateParams.orientation)
|
|
2808
|
+
: undefined;
|
|
2809
|
+
// Transform target position from XR-origin-relative to GlobalSpace
|
|
2810
|
+
const targetPosition = animateParams.position
|
|
2811
|
+
? this.transformXROriginToGlobal(animateParams.position)
|
|
2812
|
+
: undefined;
|
|
2813
|
+
return this.enqueueDuration(method, params, durationMs, {
|
|
2814
|
+
position: animateParams.position
|
|
2815
|
+
? currentTransform.position
|
|
2816
|
+
: undefined,
|
|
2817
|
+
orientation: targetOrientation
|
|
2818
|
+
? currentTransform.orientation
|
|
2819
|
+
: undefined,
|
|
2820
|
+
}, {
|
|
2821
|
+
position: targetPosition,
|
|
2822
|
+
orientation: targetOrientation,
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
// Handle select specially - it's a discrete action that enqueues multiple sub-actions
|
|
2826
|
+
if (method === 'select') {
|
|
2827
|
+
const selectParams = params;
|
|
2828
|
+
return this.executeSelectSequence(selectParams);
|
|
2829
|
+
}
|
|
2830
|
+
// All other methods are discrete actions that go through the queue
|
|
2831
|
+
return this.enqueueDiscrete(method, params);
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* Execute an immediate method synchronously (not queued).
|
|
2835
|
+
* Used for queries and session management that must work outside XR frames.
|
|
2836
|
+
*/
|
|
2837
|
+
executeImmediateMethod(method, params) {
|
|
2838
|
+
switch (method) {
|
|
2839
|
+
case 'get_session_status':
|
|
2840
|
+
return this.executeGetSessionStatus();
|
|
2841
|
+
case 'accept_session':
|
|
2842
|
+
return this.executeAcceptSession();
|
|
2843
|
+
case 'end_session':
|
|
2844
|
+
return this.executeEndSession();
|
|
2845
|
+
case 'get_transform':
|
|
2846
|
+
return this.executeGetTransform(params);
|
|
2847
|
+
case 'get_select_value':
|
|
2848
|
+
return this.executeGetSelectValue(params);
|
|
2849
|
+
case 'get_gamepad_state':
|
|
2850
|
+
return this.executeGetGamepadState(params);
|
|
2851
|
+
case 'get_device_state':
|
|
2852
|
+
return this.executeGetDeviceState();
|
|
2853
|
+
case 'capture_canvas':
|
|
2854
|
+
return this.executeCaptureCanvas(params);
|
|
2855
|
+
default:
|
|
2856
|
+
throw new Error(`Unknown immediate method: ${method}`);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
/**
|
|
2860
|
+
* Execute select action - this directly enqueues the three sub-actions without awaiting
|
|
2861
|
+
* The caller's promise resolves when all sub-actions complete
|
|
2862
|
+
*/
|
|
2863
|
+
executeSelectSequence(params) {
|
|
2864
|
+
const { device: deviceId, duration = 0.15 } = params;
|
|
2865
|
+
return new Promise((resolve, reject) => {
|
|
2866
|
+
// Track completion of all three actions
|
|
2867
|
+
let actionsCompleted = 0;
|
|
2868
|
+
const totalActions = 3;
|
|
2869
|
+
const checkComplete = () => {
|
|
2870
|
+
actionsCompleted++;
|
|
2871
|
+
if (actionsCompleted === totalActions) {
|
|
2872
|
+
resolve({
|
|
2873
|
+
device: deviceId,
|
|
2874
|
+
duration,
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
2877
|
+
};
|
|
2878
|
+
// Enqueue: set value to 1
|
|
2879
|
+
const action1 = {
|
|
2880
|
+
type: 'discrete',
|
|
2881
|
+
id: this.generateActionId(),
|
|
2882
|
+
method: '_select_press',
|
|
2883
|
+
params: { device: deviceId },
|
|
2884
|
+
resolve: checkComplete,
|
|
2885
|
+
reject,
|
|
2886
|
+
};
|
|
2887
|
+
// Enqueue: wait for duration
|
|
2888
|
+
const action2 = {
|
|
2889
|
+
type: 'duration',
|
|
2890
|
+
id: this.generateActionId(),
|
|
2891
|
+
method: '_select_wait',
|
|
2892
|
+
params: { device: deviceId },
|
|
2893
|
+
durationMs: duration * 1000,
|
|
2894
|
+
elapsedMs: 0,
|
|
2895
|
+
startState: {},
|
|
2896
|
+
targetState: {},
|
|
2897
|
+
resolve: checkComplete,
|
|
2898
|
+
reject,
|
|
2899
|
+
};
|
|
2900
|
+
// Enqueue: set value to 0
|
|
2901
|
+
const action3 = {
|
|
2902
|
+
type: 'discrete',
|
|
2903
|
+
id: this.generateActionId(),
|
|
2904
|
+
method: '_select_release',
|
|
2905
|
+
params: { device: deviceId },
|
|
2906
|
+
resolve: checkComplete,
|
|
2907
|
+
reject,
|
|
2908
|
+
};
|
|
2909
|
+
this.commandQueue.push(action1, action2, action3);
|
|
2910
|
+
});
|
|
2911
|
+
}
|
|
2912
|
+
/**
|
|
2913
|
+
* Accept an offered XR session (async wrapper for proper session activation)
|
|
2914
|
+
*/
|
|
2915
|
+
async acceptSession() {
|
|
2916
|
+
if (!this.device.sessionOffered) {
|
|
2917
|
+
throw new Error('No session has been offered');
|
|
2918
|
+
}
|
|
2919
|
+
this.device.grantOfferedSession();
|
|
2920
|
+
// Wait for session to become active
|
|
2921
|
+
await waitForCondition(() => !!this.device.activeSession, 5000);
|
|
2922
|
+
// Just return success - caller can use get_session_status for details
|
|
2923
|
+
return { success: true };
|
|
2924
|
+
}
|
|
2925
|
+
/**
|
|
2926
|
+
* Force release capture mode (for testing/cleanup)
|
|
2927
|
+
*/
|
|
2928
|
+
forceRelease() {
|
|
2929
|
+
this.cancelReleaseTimer();
|
|
2930
|
+
this._isCaptured = false;
|
|
2931
|
+
this.device.controlMode = 'manual';
|
|
2932
|
+
// Clear pending actions
|
|
2933
|
+
for (const action of this.commandQueue) {
|
|
2934
|
+
action.reject(new Error('Capture released'));
|
|
2935
|
+
}
|
|
2936
|
+
this.commandQueue = [];
|
|
2937
|
+
// Reset any stuck select/trigger values
|
|
2938
|
+
for (const hand of ['left', 'right']) {
|
|
2939
|
+
const controller = this.device.controllers[hand];
|
|
2940
|
+
if (controller) {
|
|
2941
|
+
controller.updateButtonValue('trigger', 0);
|
|
2942
|
+
controller.updateButtonValue('squeeze', 0);
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
// =============================================================================
|
|
2948
|
+
// Public API - Dispatch
|
|
2949
|
+
// =============================================================================
|
|
2950
|
+
/**
|
|
2951
|
+
* Set of methods that execute immediately (synchronously) without going through the queue.
|
|
2952
|
+
* These are queries and session management commands that need to work outside of XR frames.
|
|
2953
|
+
*/
|
|
2954
|
+
RemoteControlInterface.IMMEDIATE_METHODS = new Set([
|
|
2955
|
+
// Session management - must work before/after XR session
|
|
2956
|
+
'get_session_status',
|
|
2957
|
+
'accept_session',
|
|
2958
|
+
'end_session',
|
|
2959
|
+
// Pure queries - just read current state
|
|
2960
|
+
'get_transform',
|
|
2961
|
+
'get_select_value',
|
|
2962
|
+
'get_gamepad_state',
|
|
2963
|
+
'get_device_state',
|
|
2964
|
+
// Canvas capture - reads current canvas state
|
|
2965
|
+
'capture_canvas',
|
|
2966
|
+
]);
|
|
2967
|
+
/**
|
|
2968
|
+
* Set of immediate methods that are "active" - they modify state and should trigger capture mode.
|
|
2969
|
+
* Passive methods (queries) should NOT trigger capture mode.
|
|
2970
|
+
*/
|
|
2971
|
+
RemoteControlInterface.ACTIVE_IMMEDIATE_METHODS = new Set([
|
|
2972
|
+
'accept_session',
|
|
2973
|
+
'end_session',
|
|
2974
|
+
]);
|
|
2975
|
+
/**
|
|
2976
|
+
* Set of methods that require an active XR session.
|
|
2977
|
+
* These are state-modifying methods that are processed during frame updates.
|
|
2978
|
+
*/
|
|
2979
|
+
RemoteControlInterface.SESSION_REQUIRED_METHODS = new Set([
|
|
2980
|
+
'set_transform',
|
|
2981
|
+
'look_at',
|
|
2982
|
+
'animate_to',
|
|
2983
|
+
'set_input_mode',
|
|
2984
|
+
'set_connected',
|
|
2985
|
+
'set_select_value',
|
|
2986
|
+
'select',
|
|
2987
|
+
'set_gamepad_state',
|
|
2988
|
+
'set_device_state',
|
|
2989
|
+
]);
|
|
2990
|
+
|
|
1541
2991
|
/**
|
|
1542
2992
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
1543
2993
|
*
|
|
@@ -1884,6 +3334,31 @@
|
|
|
1884
3334
|
console.warn(`Current controller does not have button ${id}.`);
|
|
1885
3335
|
}
|
|
1886
3336
|
}
|
|
3337
|
+
/**
|
|
3338
|
+
* Set button value immediately (bypasses pending mechanism).
|
|
3339
|
+
* Use this for programmatic control where value should be readable immediately.
|
|
3340
|
+
*/
|
|
3341
|
+
setButtonValueImmediate(id, value) {
|
|
3342
|
+
if (value > 1 || value < 0) {
|
|
3343
|
+
console.warn(`Out-of-range value ${value} provided for button ${id}.`);
|
|
3344
|
+
return;
|
|
3345
|
+
}
|
|
3346
|
+
const gamepadButton = this[P_TRACKED_INPUT].inputSource.gamepad[P_GAMEPAD].buttonsMap[id];
|
|
3347
|
+
if (gamepadButton) {
|
|
3348
|
+
if (gamepadButton[P_GAMEPAD].type === 'binary' &&
|
|
3349
|
+
value != 1 &&
|
|
3350
|
+
value != 0) {
|
|
3351
|
+
console.warn(`Non-binary value ${value} provided for binary button ${id}.`);
|
|
3352
|
+
return;
|
|
3353
|
+
}
|
|
3354
|
+
// Set both value and pendingValue for immediate effect
|
|
3355
|
+
gamepadButton[P_GAMEPAD].value = value;
|
|
3356
|
+
gamepadButton[P_GAMEPAD].pendingValue = value;
|
|
3357
|
+
}
|
|
3358
|
+
else {
|
|
3359
|
+
console.warn(`Current controller does not have button ${id}.`);
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
1887
3362
|
updateButtonTouch(id, touched) {
|
|
1888
3363
|
const gamepadButton = this[P_TRACKED_INPUT].inputSource.gamepad[P_GAMEPAD].buttonsMap[id];
|
|
1889
3364
|
if (gamepadButton) {
|
|
@@ -1925,6 +3400,37 @@
|
|
|
1925
3400
|
console.warn(`Current controller does not have ${id} axes.`);
|
|
1926
3401
|
}
|
|
1927
3402
|
}
|
|
3403
|
+
/**
|
|
3404
|
+
* Get the current value of a button by id
|
|
3405
|
+
*/
|
|
3406
|
+
getButtonValue(id) {
|
|
3407
|
+
var _a;
|
|
3408
|
+
const gamepadButton = this[P_TRACKED_INPUT].inputSource.gamepad[P_GAMEPAD].buttonsMap[id];
|
|
3409
|
+
if (gamepadButton) {
|
|
3410
|
+
return (_a = gamepadButton[P_GAMEPAD].pendingValue) !== null && _a !== void 0 ? _a : gamepadButton.value;
|
|
3411
|
+
}
|
|
3412
|
+
return 0;
|
|
3413
|
+
}
|
|
3414
|
+
/**
|
|
3415
|
+
* Get the touched state of a button by id
|
|
3416
|
+
*/
|
|
3417
|
+
getButtonTouched(id) {
|
|
3418
|
+
const gamepadButton = this[P_TRACKED_INPUT].inputSource.gamepad[P_GAMEPAD].buttonsMap[id];
|
|
3419
|
+
if (gamepadButton) {
|
|
3420
|
+
return gamepadButton.touched;
|
|
3421
|
+
}
|
|
3422
|
+
return false;
|
|
3423
|
+
}
|
|
3424
|
+
/**
|
|
3425
|
+
* Get the current axes values for a given id (e.g., 'thumbstick')
|
|
3426
|
+
*/
|
|
3427
|
+
getAxes(id = 'thumbstick') {
|
|
3428
|
+
const axesById = this[P_TRACKED_INPUT].inputSource.gamepad[P_GAMEPAD].axesMap[id];
|
|
3429
|
+
if (axesById) {
|
|
3430
|
+
return { x: axesById.x, y: axesById.y };
|
|
3431
|
+
}
|
|
3432
|
+
return { x: 0, y: 0 };
|
|
3433
|
+
}
|
|
1928
3434
|
}
|
|
1929
3435
|
|
|
1930
3436
|
/**
|
|
@@ -4181,9 +5687,9 @@
|
|
|
4181
5687
|
getTranslation(toPosition, toMatrix);
|
|
4182
5688
|
getRotation(toQuaternion, toMatrix);
|
|
4183
5689
|
getScaling(toScale, toMatrix);
|
|
4184
|
-
lerp(interpolatedPosition, fromPosition, toPosition, alpha);
|
|
5690
|
+
lerp$1(interpolatedPosition, fromPosition, toPosition, alpha);
|
|
4185
5691
|
slerp(interpolatedQuaternion, fromQuaternion, toQuaternion, alpha);
|
|
4186
|
-
lerp(interpolatedScale, fromScale, toScale, alpha);
|
|
5692
|
+
lerp$1(interpolatedScale, fromScale, toScale, alpha);
|
|
4187
5693
|
fromRotationTranslationScale(out, interpolatedQuaternion, interpolatedPosition, interpolatedScale);
|
|
4188
5694
|
return out;
|
|
4189
5695
|
};
|
|
@@ -4508,7 +6014,7 @@
|
|
|
4508
6014
|
const f1q = fromValues(lastTransform[3], lastTransform[4], lastTransform[5], lastTransform[6]);
|
|
4509
6015
|
const f2p = fromValues$2(nextTransform[0], nextTransform[1], nextTransform[2]);
|
|
4510
6016
|
const f2q = fromValues(nextTransform[3], nextTransform[4], nextTransform[5], nextTransform[6]);
|
|
4511
|
-
lerp(this[P_ACTION_PLAYER].vec3, f1p, f2p, alpha);
|
|
6017
|
+
lerp$1(this[P_ACTION_PLAYER].vec3, f1p, f2p, alpha);
|
|
4512
6018
|
slerp(this[P_ACTION_PLAYER].quat, f1q, f2q, alpha);
|
|
4513
6019
|
fromRotationTranslation(space[P_SPACE].offsetMatrix, this[P_ACTION_PLAYER].quat, this[P_ACTION_PLAYER].vec3);
|
|
4514
6020
|
}
|
|
@@ -4533,7 +6039,7 @@
|
|
|
4533
6039
|
}
|
|
4534
6040
|
}
|
|
4535
6041
|
|
|
4536
|
-
const VERSION = "2.
|
|
6042
|
+
const VERSION = "2.2.0";
|
|
4537
6043
|
|
|
4538
6044
|
/**
|
|
4539
6045
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
@@ -7840,6 +9346,16 @@ void main() {
|
|
|
7840
9346
|
},
|
|
7841
9347
|
onFrameStart: (frame) => {
|
|
7842
9348
|
var _a;
|
|
9349
|
+
// Calculate delta time for remote control
|
|
9350
|
+
const now = performance.now();
|
|
9351
|
+
const deltaTimeMs = this[P_DEVICE].lastFrameTime > 0
|
|
9352
|
+
? now - this[P_DEVICE].lastFrameTime
|
|
9353
|
+
: 16.67; // Default to ~60fps
|
|
9354
|
+
this[P_DEVICE].lastFrameTime = now;
|
|
9355
|
+
// Update remote control interface
|
|
9356
|
+
if (this[P_DEVICE].remote) {
|
|
9357
|
+
this[P_DEVICE].remote.update(deltaTimeMs);
|
|
9358
|
+
}
|
|
7843
9359
|
if ((_a = this[P_DEVICE].actionPlayer) === null || _a === void 0 ? void 0 : _a.playing) {
|
|
7844
9360
|
this[P_DEVICE].actionPlayer.playFrame();
|
|
7845
9361
|
}
|
|
@@ -7873,7 +9389,17 @@ void main() {
|
|
|
7873
9389
|
}
|
|
7874
9390
|
this[P_DEVICE].updateViews();
|
|
7875
9391
|
},
|
|
9392
|
+
// control mode for programmatic access
|
|
9393
|
+
controlMode: 'manual',
|
|
9394
|
+
controlModeListeners: new Set(),
|
|
9395
|
+
stateChangeListeners: new Set(),
|
|
9396
|
+
// remote control interface - initialized after this object
|
|
9397
|
+
remote: null,
|
|
9398
|
+
// frame timing for remote update
|
|
9399
|
+
lastFrameTime: 0,
|
|
7876
9400
|
};
|
|
9401
|
+
// Initialize remote control interface
|
|
9402
|
+
this[P_DEVICE].remote = new RemoteControlInterface(this);
|
|
7877
9403
|
this[P_DEVICE].updateViews();
|
|
7878
9404
|
}
|
|
7879
9405
|
installRuntime(options) {
|
|
@@ -8036,6 +9562,14 @@ void main() {
|
|
|
8036
9562
|
}
|
|
8037
9563
|
return;
|
|
8038
9564
|
}
|
|
9565
|
+
/**
|
|
9566
|
+
* Get the app canvas when an XR session is active.
|
|
9567
|
+
* Returns undefined if no session is active or no canvas is available.
|
|
9568
|
+
*/
|
|
9569
|
+
get appCanvas() {
|
|
9570
|
+
var _a;
|
|
9571
|
+
return (_a = this[P_DEVICE].canvasData) === null || _a === void 0 ? void 0 : _a.canvas;
|
|
9572
|
+
}
|
|
8039
9573
|
get activeSession() {
|
|
8040
9574
|
var _a;
|
|
8041
9575
|
return (_a = this[P_DEVICE].xrSystem) === null || _a === void 0 ? void 0 : _a[P_SYSTEM].activeSession;
|
|
@@ -8101,6 +9635,65 @@ void main() {
|
|
|
8101
9635
|
get sem() {
|
|
8102
9636
|
return this[P_DEVICE].sem;
|
|
8103
9637
|
}
|
|
9638
|
+
get remote() {
|
|
9639
|
+
return this[P_DEVICE].remote;
|
|
9640
|
+
}
|
|
9641
|
+
// =============================================================================
|
|
9642
|
+
// Control Mode API
|
|
9643
|
+
// =============================================================================
|
|
9644
|
+
/**
|
|
9645
|
+
* Get the current control mode
|
|
9646
|
+
* - 'manual': User controls device via DevUI (default)
|
|
9647
|
+
* - 'programmatic': External API controls device
|
|
9648
|
+
*/
|
|
9649
|
+
get controlMode() {
|
|
9650
|
+
return this[P_DEVICE].controlMode;
|
|
9651
|
+
}
|
|
9652
|
+
/**
|
|
9653
|
+
* Set the control mode
|
|
9654
|
+
* Notifies all registered listeners of the change
|
|
9655
|
+
*/
|
|
9656
|
+
set controlMode(mode) {
|
|
9657
|
+
if (mode !== 'manual' && mode !== 'programmatic') {
|
|
9658
|
+
console.warn('control mode can only be "manual" or "programmatic"');
|
|
9659
|
+
return;
|
|
9660
|
+
}
|
|
9661
|
+
const prevMode = this[P_DEVICE].controlMode;
|
|
9662
|
+
if (prevMode !== mode) {
|
|
9663
|
+
this[P_DEVICE].controlMode = mode;
|
|
9664
|
+
this[P_DEVICE].controlModeListeners.forEach((listener) => listener(mode));
|
|
9665
|
+
}
|
|
9666
|
+
}
|
|
9667
|
+
/**
|
|
9668
|
+
* Register a listener to be notified when control mode changes
|
|
9669
|
+
* @param listener - Callback function that receives the new mode
|
|
9670
|
+
* @returns Unsubscribe function to remove the listener
|
|
9671
|
+
*/
|
|
9672
|
+
onControlModeChange(listener) {
|
|
9673
|
+
this[P_DEVICE].controlModeListeners.add(listener);
|
|
9674
|
+
return () => {
|
|
9675
|
+
this[P_DEVICE].controlModeListeners.delete(listener);
|
|
9676
|
+
};
|
|
9677
|
+
}
|
|
9678
|
+
/**
|
|
9679
|
+
* Register a listener to be notified when device state changes
|
|
9680
|
+
* Called after programmatic state modifications
|
|
9681
|
+
* @param listener - Callback function
|
|
9682
|
+
* @returns Unsubscribe function to remove the listener
|
|
9683
|
+
*/
|
|
9684
|
+
onStateChange(listener) {
|
|
9685
|
+
this[P_DEVICE].stateChangeListeners.add(listener);
|
|
9686
|
+
return () => {
|
|
9687
|
+
this[P_DEVICE].stateChangeListeners.delete(listener);
|
|
9688
|
+
};
|
|
9689
|
+
}
|
|
9690
|
+
/**
|
|
9691
|
+
* Notify all state change listeners that device state has been modified
|
|
9692
|
+
* Should be called after programmatic state modifications
|
|
9693
|
+
*/
|
|
9694
|
+
notifyStateChange() {
|
|
9695
|
+
this[P_DEVICE].stateChangeListeners.forEach((listener) => listener());
|
|
9696
|
+
}
|
|
8104
9697
|
}
|
|
8105
9698
|
|
|
8106
9699
|
/**
|
|
@@ -8564,6 +10157,7 @@ void main() {
|
|
|
8564
10157
|
exports.P_VIEWER_POSE = P_VIEWER_POSE;
|
|
8565
10158
|
exports.P_VIEWPORT = P_VIEWPORT;
|
|
8566
10159
|
exports.P_WEBGL_LAYER = P_WEBGL_LAYER;
|
|
10160
|
+
exports.RemoteControlInterface = RemoteControlInterface;
|
|
8567
10161
|
exports.XRAnchor = XRAnchor;
|
|
8568
10162
|
exports.XRAnchorSet = XRAnchorSet;
|
|
8569
10163
|
exports.XRDevice = XRDevice;
|
|
@@ -8594,10 +10188,17 @@ void main() {
|
|
|
8594
10188
|
exports.XRViewerPose = XRViewerPose;
|
|
8595
10189
|
exports.XRViewport = XRViewport;
|
|
8596
10190
|
exports.XRWebGLLayer = XRWebGLLayer;
|
|
10191
|
+
exports.directionTo = directionTo;
|
|
10192
|
+
exports.eulerToQuat = eulerToQuat;
|
|
10193
|
+
exports.lookRotation = lookRotation;
|
|
8597
10194
|
exports.metaQuest2 = metaQuest2;
|
|
8598
10195
|
exports.metaQuest3 = metaQuest3;
|
|
8599
10196
|
exports.metaQuestPro = metaQuestPro;
|
|
8600
10197
|
exports.oculusQuest1 = oculusQuest1;
|
|
10198
|
+
exports.quatToEuler = quatToEuler;
|
|
10199
|
+
exports.quatToObj = quatToObj;
|
|
10200
|
+
exports.vec3ToObj = vec3ToObj;
|
|
10201
|
+
exports.waitForCondition = waitForCondition;
|
|
8601
10202
|
|
|
8602
10203
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
8603
10204
|
|