@terraware/web-components 4.2.21 → 4.2.22
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/components/VirtualWalkthrough/Annotation.d.ts.map +1 -1
- package/components/VirtualWalkthrough/Annotation.js +10 -0
- package/components/VirtualWalkthrough/BoundaryWall.d.ts +11 -0
- package/components/VirtualWalkthrough/BoundaryWall.d.ts.map +1 -0
- package/components/VirtualWalkthrough/BoundaryWall.js +35 -0
- package/components/VirtualWalkthrough/TfXrNavigation.d.ts +42 -6
- package/components/VirtualWalkthrough/TfXrNavigation.d.ts.map +1 -1
- package/components/VirtualWalkthrough/TfXrNavigation.js +109 -9
- package/components/VirtualWalkthrough/TfXrNavigation.test.js +110 -0
- package/components/VirtualWalkthrough/VirtualWalkthroughViewer.d.ts.map +1 -1
- package/components/VirtualWalkthrough/VirtualWalkthroughViewer.js +50 -3
- package/components/VirtualWalkthrough/VrAnnotationPanel.d.ts +9 -0
- package/components/VirtualWalkthrough/VrAnnotationPanel.d.ts.map +1 -0
- package/components/VirtualWalkthrough/VrAnnotationPanel.js +22 -0
- package/components/VirtualWalkthrough/XrAnnotationInteraction.d.ts +6 -0
- package/components/VirtualWalkthrough/XrAnnotationInteraction.d.ts.map +1 -0
- package/components/VirtualWalkthrough/XrAnnotationInteraction.js +15 -0
- package/components/VirtualWalkthrough/XrGazeDwell.d.ts +6 -0
- package/components/VirtualWalkthrough/XrGazeDwell.d.ts.map +1 -0
- package/components/VirtualWalkthrough/XrGazeDwell.js +15 -0
- package/components/VirtualWalkthrough/XrPointerRay.d.ts +7 -0
- package/components/VirtualWalkthrough/XrPointerRay.d.ts.map +1 -0
- package/components/VirtualWalkthrough/XrPointerRay.js +15 -0
- package/components/VirtualWalkthrough/boundary-wall.d.ts +86 -0
- package/components/VirtualWalkthrough/boundary-wall.d.ts.map +1 -0
- package/components/VirtualWalkthrough/boundary-wall.js +331 -0
- package/components/VirtualWalkthrough/boundary-wall.test.js +203 -0
- package/components/VirtualWalkthrough/vr-annotation-panel-layout.d.ts +6 -0
- package/components/VirtualWalkthrough/vr-annotation-panel-layout.d.ts.map +1 -0
- package/components/VirtualWalkthrough/vr-annotation-panel-layout.js +22 -0
- package/components/VirtualWalkthrough/vr-annotation-panel-layout.test.js +26 -0
- package/components/VirtualWalkthrough/vr-annotation-panel.d.ts +40 -0
- package/components/VirtualWalkthrough/vr-annotation-panel.d.ts.map +1 -0
- package/components/VirtualWalkthrough/vr-annotation-panel.js +312 -0
- package/components/VirtualWalkthrough/xr-annotation-candidates.d.ts +18 -0
- package/components/VirtualWalkthrough/xr-annotation-candidates.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-annotation-candidates.js +38 -0
- package/components/VirtualWalkthrough/xr-annotation-interaction.d.ts +13 -0
- package/components/VirtualWalkthrough/xr-annotation-interaction.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-annotation-interaction.js +59 -0
- package/components/VirtualWalkthrough/xr-annotation-targeting.d.ts +10 -0
- package/components/VirtualWalkthrough/xr-annotation-targeting.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-annotation-targeting.js +36 -0
- package/components/VirtualWalkthrough/xr-annotation-targeting.test.js +66 -0
- package/components/VirtualWalkthrough/xr-button-arming.d.ts +14 -0
- package/components/VirtualWalkthrough/xr-button-arming.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-button-arming.js +19 -0
- package/components/VirtualWalkthrough/xr-button-arming.test.js +35 -0
- package/components/VirtualWalkthrough/xr-button-hold.d.ts +20 -0
- package/components/VirtualWalkthrough/xr-button-hold.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-button-hold.js +43 -0
- package/components/VirtualWalkthrough/xr-button-hold.test.js +57 -0
- package/components/VirtualWalkthrough/xr-exit-button.d.ts +21 -4
- package/components/VirtualWalkthrough/xr-exit-button.d.ts.map +1 -1
- package/components/VirtualWalkthrough/xr-exit-button.js +98 -37
- package/components/VirtualWalkthrough/xr-face-buttons.d.ts +15 -0
- package/components/VirtualWalkthrough/xr-face-buttons.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-face-buttons.js +31 -0
- package/components/VirtualWalkthrough/xr-face-buttons.test.js +82 -0
- package/components/VirtualWalkthrough/xr-gaze-dwell-state.d.ts +17 -0
- package/components/VirtualWalkthrough/xr-gaze-dwell-state.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-gaze-dwell-state.js +39 -0
- package/components/VirtualWalkthrough/xr-gaze-dwell-state.test.js +61 -0
- package/components/VirtualWalkthrough/xr-gaze-dwell.d.ts +30 -0
- package/components/VirtualWalkthrough/xr-gaze-dwell.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-gaze-dwell.js +173 -0
- package/components/VirtualWalkthrough/xr-interactive-ui.d.ts +13 -0
- package/components/VirtualWalkthrough/xr-interactive-ui.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-interactive-ui.js +25 -0
- package/components/VirtualWalkthrough/xr-interactive-ui.test.js +165 -0
- package/components/VirtualWalkthrough/xr-pointer-ray.d.ts +10 -0
- package/components/VirtualWalkthrough/xr-pointer-ray.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-pointer-ray.js +24 -0
- package/components/VirtualWalkthrough/xr-progress-pie.d.ts +10 -0
- package/components/VirtualWalkthrough/xr-progress-pie.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-progress-pie.js +30 -0
- package/components/VirtualWalkthrough/xr-progress-pie.test.js +79 -0
- package/components/VirtualWalkthrough/xr-ray-quad.d.ts +13 -0
- package/components/VirtualWalkthrough/xr-ray-quad.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-ray-quad.js +29 -0
- package/components/VirtualWalkthrough/xr-ray-quad.test.js +37 -0
- package/components/VirtualWalkthrough/xr-scene-bounds.d.ts +17 -0
- package/components/VirtualWalkthrough/xr-scene-bounds.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-scene-bounds.js +41 -0
- package/components/VirtualWalkthrough/xr-scene-bounds.test.js +70 -0
- package/components/VirtualWalkthrough/xr-teleport-gesture.d.ts +30 -0
- package/components/VirtualWalkthrough/xr-teleport-gesture.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-teleport-gesture.js +45 -0
- package/components/VirtualWalkthrough/xr-teleport-gesture.test.js +90 -0
- package/hooks/useXr.d.ts +2 -0
- package/hooks/useXr.d.ts.map +1 -1
- package/hooks/useXr.js +10 -6
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Vec3 } from 'playcanvas';
|
|
2
|
+
/**
|
|
3
|
+
* Intersects the ray with the rectangle centred at `center` and spanned by unit axes `right`/`up`,
|
|
4
|
+
* returning the hit in normalized quad coordinates (u,v in [-1,1]) or null. `u`/`v` divide the
|
|
5
|
+
* in-plane offset by `halfWidth`/`halfHeight`. Null when the ray is parallel to the plane, the
|
|
6
|
+
* plane is behind the origin, or the hit lands outside the rectangle.
|
|
7
|
+
*/
|
|
8
|
+
export const rayQuadHit = (origin, direction, center, right, up, halfWidth, halfHeight) => {
|
|
9
|
+
const dir = direction.clone().normalize();
|
|
10
|
+
const normal = new Vec3().cross(right, up);
|
|
11
|
+
const denom = dir.dot(normal);
|
|
12
|
+
if (Math.abs(denom) < 1e-6) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const t = new Vec3().sub2(center, origin).dot(normal) / denom;
|
|
16
|
+
if (t < 0) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const offset = new Vec3().copy(dir).mulScalar(t).add(origin).sub(center);
|
|
20
|
+
const u = offset.dot(right) / halfWidth;
|
|
21
|
+
const v = offset.dot(up) / halfHeight;
|
|
22
|
+
if (u < -1 || u > 1 || v < -1 || v > 1) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
u,
|
|
27
|
+
v
|
|
28
|
+
};
|
|
29
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Vec3 } from 'playcanvas';
|
|
2
|
+
import { rayQuadHit } from './xr-ray-quad';
|
|
3
|
+
const CENTER = new Vec3(0, 0, 0);
|
|
4
|
+
const RIGHT = new Vec3(1, 0, 0);
|
|
5
|
+
const UP = new Vec3(0, 1, 0);
|
|
6
|
+
describe('rayQuadHit', () => {
|
|
7
|
+
it('hits the centre straight on', () => {
|
|
8
|
+
const hit = rayQuadHit(new Vec3(0, 0, 5), new Vec3(0, 0, -1), CENTER, RIGHT, UP, 1, 1);
|
|
9
|
+
expect(hit).not.toBeNull();
|
|
10
|
+
expect(hit?.u).toBeCloseTo(0, 5);
|
|
11
|
+
expect(hit?.v).toBeCloseTo(0, 5);
|
|
12
|
+
});
|
|
13
|
+
it('maps an off-centre hit to normalized quad coordinates', () => {
|
|
14
|
+
const hit = rayQuadHit(new Vec3(0.5, -0.25, 5), new Vec3(0, 0, -1), CENTER, RIGHT, UP, 1, 1);
|
|
15
|
+
expect(hit?.u).toBeCloseTo(0.5, 5);
|
|
16
|
+
expect(hit?.v).toBeCloseTo(-0.25, 5);
|
|
17
|
+
});
|
|
18
|
+
it('returns null when the hit falls outside the rectangle', () => {
|
|
19
|
+
expect(rayQuadHit(new Vec3(2, 0, 5), new Vec3(0, 0, -1), CENTER, RIGHT, UP, 1, 1)).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
it('returns null for a ray parallel to the quad plane', () => {
|
|
22
|
+
expect(rayQuadHit(new Vec3(0, 0, 5), new Vec3(1, 0, 0), CENTER, RIGHT, UP, 1, 1)).toBeNull();
|
|
23
|
+
});
|
|
24
|
+
it('returns null when the quad is behind the ray origin', () => {
|
|
25
|
+
expect(rayQuadHit(new Vec3(0, 0, 5), new Vec3(0, 0, 1), CENTER, RIGHT, UP, 1, 1)).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
it('respects half extents (scales u/v by them)', () => {
|
|
28
|
+
// half width 2: an x offset of 1 maps to u = 0.5.
|
|
29
|
+
const hit = rayQuadHit(new Vec3(1, 0, 5), new Vec3(0, 0, -1), CENTER, RIGHT, UP, 2, 1);
|
|
30
|
+
expect(hit?.u).toBeCloseTo(0.5, 5);
|
|
31
|
+
});
|
|
32
|
+
it('normalizes a non-unit ray direction', () => {
|
|
33
|
+
const hit = rayQuadHit(new Vec3(0, 0, 5), new Vec3(0, 0, -3), CENTER, RIGHT, UP, 1, 1);
|
|
34
|
+
expect(hit).not.toBeNull();
|
|
35
|
+
expect(hit?.u).toBeCloseTo(0, 5);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** A world-space XZ point clamped into the bounds circle, and how far it had to move to get there. */
|
|
2
|
+
export type ClampResult = {
|
|
3
|
+
x: number;
|
|
4
|
+
z: number;
|
|
5
|
+
distance: number;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Clamp a world-space XZ point into the circle of `radius` about (centerX, centerZ).
|
|
9
|
+
*
|
|
10
|
+
* `distance` is how far the point was moved and is 0 whenever the point was already inside, which
|
|
11
|
+
* callers use as the "was this clamped?" signal. A non-positive radius means "no bounds" and leaves
|
|
12
|
+
* the point untouched; so does a point at the exact center, which has no direction to project along.
|
|
13
|
+
*/
|
|
14
|
+
export declare const clampToCircle: (x: number, z: number, centerX: number, centerZ: number, radius: number) => ClampResult;
|
|
15
|
+
/** Horizontal distance from a world-space XZ point to the circle's edge: negative inside, positive outside. */
|
|
16
|
+
export declare const distanceToCircleEdge: (x: number, z: number, centerX: number, centerZ: number, radius: number) => number;
|
|
17
|
+
//# sourceMappingURL=xr-scene-bounds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xr-scene-bounds.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/xr-scene-bounds.ts"],"names":[],"mappings":"AAAA,sGAAsG;AACtG,MAAM,MAAM,WAAW,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAErE;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAI,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,SAAS,MAAM,EAAE,SAAS,MAAM,EAAE,QAAQ,MAAM,KAAG,WAetG,CAAC;AAEF,+GAA+G;AAC/G,eAAO,MAAM,oBAAoB,GAC/B,GAAG,MAAM,EACT,GAAG,MAAM,EACT,SAAS,MAAM,EACf,SAAS,MAAM,EACf,QAAQ,MAAM,KACb,MAKF,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** A world-space XZ point clamped into the bounds circle, and how far it had to move to get there. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Clamp a world-space XZ point into the circle of `radius` about (centerX, centerZ).
|
|
5
|
+
*
|
|
6
|
+
* `distance` is how far the point was moved and is 0 whenever the point was already inside, which
|
|
7
|
+
* callers use as the "was this clamped?" signal. A non-positive radius means "no bounds" and leaves
|
|
8
|
+
* the point untouched; so does a point at the exact center, which has no direction to project along.
|
|
9
|
+
*/
|
|
10
|
+
export const clampToCircle = (x, z, centerX, centerZ, radius) => {
|
|
11
|
+
if (radius <= 0) {
|
|
12
|
+
return {
|
|
13
|
+
x,
|
|
14
|
+
z,
|
|
15
|
+
distance: 0
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const dx = x - centerX;
|
|
19
|
+
const dz = z - centerZ;
|
|
20
|
+
const dist = Math.sqrt(dx * dx + dz * dz);
|
|
21
|
+
if (dist <= radius) {
|
|
22
|
+
return {
|
|
23
|
+
x,
|
|
24
|
+
z,
|
|
25
|
+
distance: 0
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const scale = radius / dist;
|
|
29
|
+
return {
|
|
30
|
+
x: centerX + dx * scale,
|
|
31
|
+
z: centerZ + dz * scale,
|
|
32
|
+
distance: dist - radius
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Horizontal distance from a world-space XZ point to the circle's edge: negative inside, positive outside. */
|
|
37
|
+
export const distanceToCircleEdge = (x, z, centerX, centerZ, radius) => {
|
|
38
|
+
const dx = x - centerX;
|
|
39
|
+
const dz = z - centerZ;
|
|
40
|
+
return Math.sqrt(dx * dx + dz * dz) - radius;
|
|
41
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { clampToCircle, distanceToCircleEdge } from './xr-scene-bounds';
|
|
2
|
+
describe('clampToCircle', () => {
|
|
3
|
+
it('leaves a point inside the circle untouched', () => {
|
|
4
|
+
expect(clampToCircle(1, 2, 0, 0, 5)).toEqual({
|
|
5
|
+
x: 1,
|
|
6
|
+
z: 2,
|
|
7
|
+
distance: 0
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
it('leaves a point exactly on the edge untouched', () => {
|
|
11
|
+
expect(clampToCircle(5, 0, 0, 0, 5)).toEqual({
|
|
12
|
+
x: 5,
|
|
13
|
+
z: 0,
|
|
14
|
+
distance: 0
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
it('projects a point outside back onto the edge', () => {
|
|
18
|
+
const result = clampToCircle(10, 0, 0, 0, 4);
|
|
19
|
+
expect(result.x).toBeCloseTo(4);
|
|
20
|
+
expect(result.z).toBeCloseTo(0);
|
|
21
|
+
expect(result.distance).toBeCloseTo(6);
|
|
22
|
+
});
|
|
23
|
+
it('projects along the line from the center, preserving direction', () => {
|
|
24
|
+
const result = clampToCircle(30, 40, 0, 0, 5);
|
|
25
|
+
expect(result.x).toBeCloseTo(3);
|
|
26
|
+
expect(result.z).toBeCloseTo(4);
|
|
27
|
+
expect(result.distance).toBeCloseTo(45);
|
|
28
|
+
});
|
|
29
|
+
it('respects an off-origin center', () => {
|
|
30
|
+
const result = clampToCircle(12, -3, 2, -3, 4);
|
|
31
|
+
expect(result.x).toBeCloseTo(6);
|
|
32
|
+
expect(result.z).toBeCloseTo(-3);
|
|
33
|
+
expect(result.distance).toBeCloseTo(6);
|
|
34
|
+
});
|
|
35
|
+
it('leaves a point at the exact center untouched rather than dividing by zero', () => {
|
|
36
|
+
const result = clampToCircle(2, -3, 2, -3, 4);
|
|
37
|
+
expect(result).toEqual({
|
|
38
|
+
x: 2,
|
|
39
|
+
z: -3,
|
|
40
|
+
distance: 0
|
|
41
|
+
});
|
|
42
|
+
expect(Number.isNaN(result.x)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it('leaves the point untouched for a non-positive radius', () => {
|
|
45
|
+
expect(clampToCircle(10, 10, 0, 0, 0)).toEqual({
|
|
46
|
+
x: 10,
|
|
47
|
+
z: 10,
|
|
48
|
+
distance: 0
|
|
49
|
+
});
|
|
50
|
+
expect(clampToCircle(10, 10, 0, 0, -1)).toEqual({
|
|
51
|
+
x: 10,
|
|
52
|
+
z: 10,
|
|
53
|
+
distance: 0
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('distanceToCircleEdge', () => {
|
|
58
|
+
it('is negative inside the circle', () => {
|
|
59
|
+
expect(distanceToCircleEdge(1, 0, 0, 0, 5)).toBeCloseTo(-4);
|
|
60
|
+
});
|
|
61
|
+
it('is zero on the edge', () => {
|
|
62
|
+
expect(distanceToCircleEdge(0, 5, 0, 0, 5)).toBeCloseTo(0);
|
|
63
|
+
});
|
|
64
|
+
it('is positive outside the circle', () => {
|
|
65
|
+
expect(distanceToCircleEdge(8, 0, 0, 0, 5)).toBeCloseTo(3);
|
|
66
|
+
});
|
|
67
|
+
it('respects an off-origin center', () => {
|
|
68
|
+
expect(distanceToCircleEdge(2, 9, 2, 3, 4)).toBeCloseTo(2);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { XrInputSource } from 'playcanvas';
|
|
2
|
+
/** Per-frame aim state for one input source, sampled while its select gesture is in progress. */
|
|
3
|
+
export interface TeleportGestureFrame {
|
|
4
|
+
pressed: boolean;
|
|
5
|
+
teleportDisabled: boolean;
|
|
6
|
+
uiBlocked: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Decides whether the select gesture that just ended is allowed to teleport. The two reasons to
|
|
10
|
+
* refuse have deliberately different lifetimes:
|
|
11
|
+
*
|
|
12
|
+
* - `teleportDisabled` latches for the rest of the press. The React flag behind it re-enables as
|
|
13
|
+
* soon as the select dismisses the panel, which can land before the selectend that ends the press.
|
|
14
|
+
* - `uiBlocked` keeps only the newest frame, because that frame is the one whose arc hit the base
|
|
15
|
+
* script has cached and would commit on release. Re-deciding from the release-time ray instead
|
|
16
|
+
* would let a flick off the UI commit a target that was aimed while over it, and latching the
|
|
17
|
+
* whole press would refuse an aim that merely swept across UI before coming to rest on floor.
|
|
18
|
+
*
|
|
19
|
+
* Callers must poll every frame for every source they care about; a press and release between polls
|
|
20
|
+
* is missed.
|
|
21
|
+
*/
|
|
22
|
+
export declare class TeleportGestureLatch {
|
|
23
|
+
private _teleportDisabled;
|
|
24
|
+
private _uiBlocked;
|
|
25
|
+
/** Records a frame of an in-progress press. A source that isn't pressed drops both latches. */
|
|
26
|
+
track(inputSource: XrInputSource, frame: TeleportGestureFrame): void;
|
|
27
|
+
/** True when the press may not teleport. Clears both latches so they never outlive the press. */
|
|
28
|
+
consumeBlocked(inputSource: XrInputSource): boolean;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=xr-teleport-gesture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xr-teleport-gesture.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/xr-teleport-gesture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,iGAAiG;AACjG,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,iBAAiB,CAAgC;IACzD,OAAO,CAAC,UAAU,CAAgC;IAElD,+FAA+F;IAC/F,KAAK,CAAC,WAAW,EAAE,aAAa,EAAE,KAAK,EAAE,oBAAoB;IAmB7D,iGAAiG;IACjG,cAAc,CAAC,WAAW,EAAE,aAAa,GAAG,OAAO;CAOpD"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Per-frame aim state for one input source, sampled while its select gesture is in progress. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Decides whether the select gesture that just ended is allowed to teleport. The two reasons to
|
|
5
|
+
* refuse have deliberately different lifetimes:
|
|
6
|
+
*
|
|
7
|
+
* - `teleportDisabled` latches for the rest of the press. The React flag behind it re-enables as
|
|
8
|
+
* soon as the select dismisses the panel, which can land before the selectend that ends the press.
|
|
9
|
+
* - `uiBlocked` keeps only the newest frame, because that frame is the one whose arc hit the base
|
|
10
|
+
* script has cached and would commit on release. Re-deciding from the release-time ray instead
|
|
11
|
+
* would let a flick off the UI commit a target that was aimed while over it, and latching the
|
|
12
|
+
* whole press would refuse an aim that merely swept across UI before coming to rest on floor.
|
|
13
|
+
*
|
|
14
|
+
* Callers must poll every frame for every source they care about; a press and release between polls
|
|
15
|
+
* is missed.
|
|
16
|
+
*/
|
|
17
|
+
export class TeleportGestureLatch {
|
|
18
|
+
_teleportDisabled = new WeakSet();
|
|
19
|
+
_uiBlocked = new WeakSet();
|
|
20
|
+
|
|
21
|
+
/** Records a frame of an in-progress press. A source that isn't pressed drops both latches. */
|
|
22
|
+
track(inputSource, frame) {
|
|
23
|
+
if (!frame.pressed) {
|
|
24
|
+
this._teleportDisabled.delete(inputSource);
|
|
25
|
+
this._uiBlocked.delete(inputSource);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (frame.teleportDisabled) {
|
|
29
|
+
this._teleportDisabled.add(inputSource);
|
|
30
|
+
}
|
|
31
|
+
if (frame.uiBlocked) {
|
|
32
|
+
this._uiBlocked.add(inputSource);
|
|
33
|
+
} else {
|
|
34
|
+
this._uiBlocked.delete(inputSource);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** True when the press may not teleport. Clears both latches so they never outlive the press. */
|
|
39
|
+
consumeBlocked(inputSource) {
|
|
40
|
+
const blocked = this._teleportDisabled.has(inputSource) || this._uiBlocked.has(inputSource);
|
|
41
|
+
this._teleportDisabled.delete(inputSource);
|
|
42
|
+
this._uiBlocked.delete(inputSource);
|
|
43
|
+
return blocked;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { TeleportGestureLatch } from './xr-teleport-gesture';
|
|
2
|
+
const source = () => ({});
|
|
3
|
+
|
|
4
|
+
/** An ordinary aiming frame: pressed, teleport on, ray clear of interactive UI. */
|
|
5
|
+
const AIMING = {
|
|
6
|
+
pressed: true,
|
|
7
|
+
teleportDisabled: false,
|
|
8
|
+
uiBlocked: false
|
|
9
|
+
};
|
|
10
|
+
describe('TeleportGestureLatch', () => {
|
|
11
|
+
it('allows a press that never aimed at UI', () => {
|
|
12
|
+
const latch = new TeleportGestureLatch();
|
|
13
|
+
const src = source();
|
|
14
|
+
latch.track(src, AIMING);
|
|
15
|
+
latch.track(src, AIMING);
|
|
16
|
+
expect(latch.consumeBlocked(src)).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
it('blocks a press whose last aiming frame was over UI, even after the ray leaves on release', () => {
|
|
19
|
+
const latch = new TeleportGestureLatch();
|
|
20
|
+
const src = source();
|
|
21
|
+
latch.track(src, {
|
|
22
|
+
...AIMING,
|
|
23
|
+
uiBlocked: true
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// The release-time ray no longer hits the UI, but the cached arc hit is from the frame above.
|
|
27
|
+
expect(latch.consumeBlocked(src)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
it('allows a press that swept across UI but came to rest on open floor', () => {
|
|
30
|
+
const latch = new TeleportGestureLatch();
|
|
31
|
+
const src = source();
|
|
32
|
+
latch.track(src, AIMING);
|
|
33
|
+
latch.track(src, {
|
|
34
|
+
...AIMING,
|
|
35
|
+
uiBlocked: true
|
|
36
|
+
});
|
|
37
|
+
latch.track(src, AIMING);
|
|
38
|
+
expect(latch.consumeBlocked(src)).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
it('latches a teleport-disabled frame for the rest of the press', () => {
|
|
41
|
+
const latch = new TeleportGestureLatch();
|
|
42
|
+
const src = source();
|
|
43
|
+
latch.track(src, {
|
|
44
|
+
...AIMING,
|
|
45
|
+
teleportDisabled: true
|
|
46
|
+
});
|
|
47
|
+
latch.track(src, AIMING);
|
|
48
|
+
expect(latch.consumeBlocked(src)).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
it('clears the latches once consumed, so the next press starts clean', () => {
|
|
51
|
+
const latch = new TeleportGestureLatch();
|
|
52
|
+
const src = source();
|
|
53
|
+
latch.track(src, {
|
|
54
|
+
pressed: true,
|
|
55
|
+
teleportDisabled: true,
|
|
56
|
+
uiBlocked: true
|
|
57
|
+
});
|
|
58
|
+
expect(latch.consumeBlocked(src)).toBe(true);
|
|
59
|
+
latch.track(src, AIMING);
|
|
60
|
+
expect(latch.consumeBlocked(src)).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
it('drops latches from a press that ended without being consumed', () => {
|
|
63
|
+
const latch = new TeleportGestureLatch();
|
|
64
|
+
const src = source();
|
|
65
|
+
latch.track(src, {
|
|
66
|
+
pressed: true,
|
|
67
|
+
teleportDisabled: true,
|
|
68
|
+
uiBlocked: true
|
|
69
|
+
});
|
|
70
|
+
latch.track(src, {
|
|
71
|
+
pressed: false,
|
|
72
|
+
teleportDisabled: true,
|
|
73
|
+
uiBlocked: true
|
|
74
|
+
});
|
|
75
|
+
latch.track(src, AIMING);
|
|
76
|
+
expect(latch.consumeBlocked(src)).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
it('tracks each input source independently', () => {
|
|
79
|
+
const latch = new TeleportGestureLatch();
|
|
80
|
+
const left = source();
|
|
81
|
+
const right = source();
|
|
82
|
+
latch.track(left, {
|
|
83
|
+
...AIMING,
|
|
84
|
+
uiBlocked: true
|
|
85
|
+
});
|
|
86
|
+
latch.track(right, AIMING);
|
|
87
|
+
expect(latch.consumeBlocked(right)).toBe(false);
|
|
88
|
+
expect(latch.consumeBlocked(left)).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
});
|
package/hooks/useXr.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export declare const useXr: ({ onError }?: UseXrOptions) => {
|
|
|
7
7
|
startXr: (type: XrType) => void;
|
|
8
8
|
endXr: () => void;
|
|
9
9
|
isCurrentlyInXr: boolean;
|
|
10
|
+
isCurrentlyInAr: boolean;
|
|
11
|
+
isCurrentlyInVr: boolean;
|
|
10
12
|
};
|
|
11
13
|
export default useXr;
|
|
12
14
|
//# sourceMappingURL=useXr.d.ts.map
|
package/hooks/useXr.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useXr.d.ts","sourceRoot":"","sources":["../../src/hooks/useXr.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;AAEjC,UAAU,YAAY;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAOD,eAAO,MAAM,KAAK,GAAI,cAAa,YAAiB;
|
|
1
|
+
{"version":3,"file":"useXr.d.ts","sourceRoot":"","sources":["../../src/hooks/useXr.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;AAEjC,UAAU,YAAY;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAOD,eAAO,MAAM,KAAK,GAAI,cAAa,YAAiB;0BA4CT,MAAM;oBAGtC,MAAM;;;;;CA6BhB,CAAC;AAEF,eAAe,KAAK,CAAC"}
|
package/hooks/useXr.js
CHANGED
|
@@ -13,7 +13,8 @@ export const useXr = ({
|
|
|
13
13
|
VR: false,
|
|
14
14
|
AR: false
|
|
15
15
|
});
|
|
16
|
-
|
|
16
|
+
// `type` is set as soon as a session is requested, so gate on `active` to ignore pending sessions.
|
|
17
|
+
const [currentXrType, setCurrentXrType] = useState(app.xr?.active ? app.xr.type : null);
|
|
17
18
|
useEffect(() => {
|
|
18
19
|
// We subscribe to the manager directly because `app` doesn't update when XR's availability changes.
|
|
19
20
|
const handleAvailable = (type, isAvailable) => {
|
|
@@ -39,9 +40,10 @@ export const useXr = ({
|
|
|
39
40
|
};
|
|
40
41
|
}, [app]);
|
|
41
42
|
useEffect(() => {
|
|
42
|
-
const handleStart = () =>
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const handleStart = () => setCurrentXrType(app.xr?.type ?? null);
|
|
44
|
+
// `app.xr.type` isn't cleared until after the `end` event fires, so don't read it here.
|
|
45
|
+
const handleEnd = () => setCurrentXrType(null);
|
|
46
|
+
setCurrentXrType(app.xr?.active ? app.xr.type : null);
|
|
45
47
|
app.xr?.on('start', handleStart);
|
|
46
48
|
app.xr?.on('end', handleEnd);
|
|
47
49
|
return () => {
|
|
@@ -68,7 +70,9 @@ export const useXr = ({
|
|
|
68
70
|
isXrAvailable,
|
|
69
71
|
startXr,
|
|
70
72
|
endXr,
|
|
71
|
-
isCurrentlyInXr
|
|
72
|
-
|
|
73
|
+
isCurrentlyInXr: currentXrType !== null,
|
|
74
|
+
isCurrentlyInAr: currentXrType === XRTYPE_AR,
|
|
75
|
+
isCurrentlyInVr: currentXrType === XRTYPE_VR
|
|
76
|
+
}), [isXrAvailable, startXr, endXr, currentXrType]);
|
|
73
77
|
};
|
|
74
78
|
export default useXr;
|