@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,35 @@
|
|
|
1
|
+
import { ButtonArmingLatch } from './xr-button-arming';
|
|
2
|
+
const source = () => ({});
|
|
3
|
+
describe('ButtonArmingLatch', () => {
|
|
4
|
+
it('never counts a source pressed from the very first poll, while it stays down', () => {
|
|
5
|
+
const latch = new ButtonArmingLatch();
|
|
6
|
+
const src = source();
|
|
7
|
+
expect(latch.trackPress(src, true)).toBe(false);
|
|
8
|
+
expect(latch.trackPress(src, true)).toBe(false);
|
|
9
|
+
expect(latch.trackPress(src, true)).toBe(false);
|
|
10
|
+
});
|
|
11
|
+
it('counts a press that follows a release', () => {
|
|
12
|
+
const latch = new ButtonArmingLatch();
|
|
13
|
+
const src = source();
|
|
14
|
+
latch.trackPress(src, true);
|
|
15
|
+
latch.trackPress(src, false);
|
|
16
|
+
expect(latch.trackPress(src, true)).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
it('tracks each input source independently, so one held from the first poll does not block another', () => {
|
|
19
|
+
const latch = new ButtonArmingLatch();
|
|
20
|
+
const held = source();
|
|
21
|
+
const other = source();
|
|
22
|
+
latch.trackPress(held, true);
|
|
23
|
+
latch.trackPress(other, false);
|
|
24
|
+
expect(latch.trackPress(held, true)).toBe(false);
|
|
25
|
+
expect(latch.trackPress(other, true)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
it('stays armed across later press/release cycles', () => {
|
|
28
|
+
const latch = new ButtonArmingLatch();
|
|
29
|
+
const src = source();
|
|
30
|
+
latch.trackPress(src, false);
|
|
31
|
+
expect(latch.trackPress(src, true)).toBe(true);
|
|
32
|
+
latch.trackPress(src, false);
|
|
33
|
+
expect(latch.trackPress(src, true)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface HoldState {
|
|
2
|
+
elapsed: number;
|
|
3
|
+
armed: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface HoldUpdate {
|
|
6
|
+
state: HoldState;
|
|
7
|
+
progress: number;
|
|
8
|
+
justFired: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const INITIAL_HOLD_STATE: HoldState;
|
|
11
|
+
/**
|
|
12
|
+
* Advances a press-and-hold timer by one frame. `held` is whether the button is down right now.
|
|
13
|
+
*
|
|
14
|
+
* A hold only counts once a release has been seen, so a button that is already down when tracking
|
|
15
|
+
* starts does nothing until the user lets go - otherwise entering a session with a thumb on the
|
|
16
|
+
* button would fire `threshold` seconds later. `justFired` is true only on the frame elapsed first
|
|
17
|
+
* reaches `threshold`, so continuing to hold fires once.
|
|
18
|
+
*/
|
|
19
|
+
export declare const advanceHold: (prev: HoldState, held: boolean, dt: number, threshold: number) => HoldUpdate;
|
|
20
|
+
//# sourceMappingURL=xr-button-hold.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xr-button-hold.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/xr-button-hold.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,eAAO,MAAM,kBAAkB,EAAE,SAAwC,CAAC;AAI1E;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,SAAS,EAAE,MAAM,OAAO,EAAE,IAAI,MAAM,EAAE,WAAW,MAAM,KAAG,UAY3F,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const INITIAL_HOLD_STATE = {
|
|
2
|
+
elapsed: 0,
|
|
3
|
+
armed: false
|
|
4
|
+
};
|
|
5
|
+
const RELEASED_STATE = {
|
|
6
|
+
elapsed: 0,
|
|
7
|
+
armed: true
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Advances a press-and-hold timer by one frame. `held` is whether the button is down right now.
|
|
12
|
+
*
|
|
13
|
+
* A hold only counts once a release has been seen, so a button that is already down when tracking
|
|
14
|
+
* starts does nothing until the user lets go - otherwise entering a session with a thumb on the
|
|
15
|
+
* button would fire `threshold` seconds later. `justFired` is true only on the frame elapsed first
|
|
16
|
+
* reaches `threshold`, so continuing to hold fires once.
|
|
17
|
+
*/
|
|
18
|
+
export const advanceHold = (prev, held, dt, threshold) => {
|
|
19
|
+
if (!held) {
|
|
20
|
+
return {
|
|
21
|
+
state: RELEASED_STATE,
|
|
22
|
+
progress: 0,
|
|
23
|
+
justFired: false
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (!prev.armed) {
|
|
27
|
+
return {
|
|
28
|
+
state: prev,
|
|
29
|
+
progress: 0,
|
|
30
|
+
justFired: false
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const elapsed = Math.min(prev.elapsed + dt, threshold);
|
|
34
|
+
const justFired = prev.elapsed < threshold && elapsed >= threshold;
|
|
35
|
+
return {
|
|
36
|
+
state: {
|
|
37
|
+
elapsed,
|
|
38
|
+
armed: true
|
|
39
|
+
},
|
|
40
|
+
progress: elapsed / threshold,
|
|
41
|
+
justFired
|
|
42
|
+
};
|
|
43
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { INITIAL_HOLD_STATE, advanceHold } from './xr-button-hold';
|
|
2
|
+
const THRESHOLD = 1.25;
|
|
3
|
+
|
|
4
|
+
/** Arms the timer the way a real release does, so a press can start accumulating. */
|
|
5
|
+
const released = () => advanceHold(INITIAL_HOLD_STATE, false, 0.1, THRESHOLD).state;
|
|
6
|
+
describe('advanceHold', () => {
|
|
7
|
+
it('ignores a button that is already held when tracking starts', () => {
|
|
8
|
+
const result = advanceHold(INITIAL_HOLD_STATE, true, 2, THRESHOLD);
|
|
9
|
+
expect(result.progress).toBe(0);
|
|
10
|
+
expect(result.justFired).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
it('stays unarmed for as long as a pre-held button is down', () => {
|
|
13
|
+
let state = INITIAL_HOLD_STATE;
|
|
14
|
+
for (let frame = 0; frame < 5; frame += 1) {
|
|
15
|
+
state = advanceHold(state, true, 1, THRESHOLD).state;
|
|
16
|
+
}
|
|
17
|
+
expect(advanceHold(state, true, 1, THRESHOLD).justFired).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
it('arms on release so the next press accumulates', () => {
|
|
20
|
+
expect(advanceHold(released(), true, 0.5, THRESHOLD).progress).toBeCloseTo(0.4);
|
|
21
|
+
});
|
|
22
|
+
it('accumulates progress across frames', () => {
|
|
23
|
+
let state = released();
|
|
24
|
+
let progress = 0;
|
|
25
|
+
for (let frame = 0; frame < 3; frame += 1) {
|
|
26
|
+
const result = advanceHold(state, true, 0.25, THRESHOLD);
|
|
27
|
+
state = result.state;
|
|
28
|
+
progress = result.progress;
|
|
29
|
+
}
|
|
30
|
+
expect(progress).toBeCloseTo(0.6);
|
|
31
|
+
});
|
|
32
|
+
it('fires once when the hold reaches the threshold', () => {
|
|
33
|
+
const partial = advanceHold(released(), true, 1, THRESHOLD).state;
|
|
34
|
+
const firing = advanceHold(partial, true, 1, THRESHOLD);
|
|
35
|
+
expect(firing.justFired).toBe(true);
|
|
36
|
+
expect(firing.progress).toBe(1);
|
|
37
|
+
expect(advanceHold(firing.state, true, 1, THRESHOLD).justFired).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
it('clamps progress to the threshold', () => {
|
|
40
|
+
const result = advanceHold(released(), true, 99, THRESHOLD);
|
|
41
|
+
expect(result.progress).toBe(1);
|
|
42
|
+
expect(result.justFired).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
it('resets to zero on release', () => {
|
|
45
|
+
const partial = advanceHold(released(), true, 1, THRESHOLD).state;
|
|
46
|
+
const result = advanceHold(partial, false, 0.1, THRESHOLD);
|
|
47
|
+
expect(result.progress).toBe(0);
|
|
48
|
+
expect(result.state.elapsed).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
it('fires again only after a fresh press', () => {
|
|
51
|
+
const fired = advanceHold(advanceHold(released(), true, 1, THRESHOLD).state, true, 1, THRESHOLD).state;
|
|
52
|
+
const afterRelease = advanceHold(fired, false, 0.1, THRESHOLD).state;
|
|
53
|
+
const rePressed = advanceHold(afterRelease, true, 1, THRESHOLD);
|
|
54
|
+
expect(rePressed.justFired).toBe(false);
|
|
55
|
+
expect(advanceHold(rePressed.state, true, 1, THRESHOLD).justFired).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -19,17 +19,34 @@ export declare class XrExitButton extends Script {
|
|
|
19
19
|
private _headPosition;
|
|
20
20
|
private _headRotation;
|
|
21
21
|
private _worldOffset;
|
|
22
|
-
private
|
|
22
|
+
private _faceButtons;
|
|
23
|
+
private _canvas?;
|
|
24
|
+
private _context?;
|
|
25
|
+
private _drawnProgress;
|
|
26
|
+
private _hold;
|
|
27
|
+
private _arming;
|
|
23
28
|
private _isVrActive;
|
|
24
29
|
private _onXrStart;
|
|
25
30
|
private _onXrEnd;
|
|
26
31
|
private _onSelect;
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
/** True when the ray points at the button's hit sphere. False while the button entity is disabled. */
|
|
33
|
+
rayHitsButton(origin: Vec3, direction: Vec3): boolean;
|
|
34
|
+
private _createCanvas;
|
|
29
35
|
private _createTexture;
|
|
36
|
+
/**
|
|
37
|
+
* Repaints the button: dark disc, then the hold progress wedge, then the X arms on top so the
|
|
38
|
+
* glyph stays legible over the fill. The pie is skipped entirely at zero progress so the idle
|
|
39
|
+
* button doesn't wear the pie's faint track. Returns whether it painted, so callers can tell a
|
|
40
|
+
* missing context from a successful repaint.
|
|
41
|
+
*/
|
|
42
|
+
private _drawButton;
|
|
43
|
+
/** Repaints only when the drawn wedge would visibly move. Only records the paint on success, so a
|
|
44
|
+
* missing context doesn't fool the dedupe cache into thinking a repaint already happened. */
|
|
45
|
+
private _setProgress;
|
|
46
|
+
private _resetProgress;
|
|
30
47
|
private _createMesh;
|
|
31
48
|
initialize(): void;
|
|
32
|
-
update(): void;
|
|
49
|
+
update(dt: number): void;
|
|
33
50
|
/**
|
|
34
51
|
* Pin the button to the upper-right of the headset view. The camera entity's transform is
|
|
35
52
|
* overwritten every frame by WalkthroughCamera and is not the rendered head pose, so the head
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xr-exit-button.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/xr-exit-button.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,MAAM,EAGN,IAAI,EAGL,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"xr-exit-button.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/xr-exit-button.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,MAAM,EAGN,IAAI,EAGL,MAAM,YAAY,CAAC;AAOpB;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,IAAI,EAAE,WAAW,IAAI,EAAE,QAAQ,IAAI,EAAE,QAAQ,MAAM,KAAG,OAUhG,CAAC;AAOF,qBAAa,YAAa,SAAQ,MAAM;IACtC,MAAM,CAAC,UAAU,SAAkB;IAEnC,yGAAyG;IACzG,MAAM,OAA8B;IAEpC,oFAAoF;IACpF,QAAQ,SAAQ;IAEhB,sGAAsG;IACtG,SAAS,SAAQ;IAEjB,OAAO,CAAC,SAAS,CAAC,CAAmB;IACrC,OAAO,CAAC,QAAQ,CAAC,CAAU;IAC3B,OAAO,CAAC,KAAK,CAAC,CAAO;IACrB,OAAO,CAAC,QAAQ,CAAS;IAEzB,OAAO,CAAC,aAAa,CAAc;IACnC,OAAO,CAAC,aAAa,CAAc;IACnC,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,YAAY,CAAgC;IAEpD,OAAO,CAAC,OAAO,CAAC,CAAoB;IACpC,OAAO,CAAC,QAAQ,CAAC,CAA2B;IAC5C,OAAO,CAAC,cAAc,CAAM;IAC5B,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,OAAO,CAA2B;IAE1C,OAAO,CAAC,WAAW,CAAyE;IAE5F,OAAO,CAAC,UAAU,CAGhB;IAEF,OAAO,CAAC,QAAQ,CASd;IAEF,OAAO,CAAC,SAAS,CAOf;IAEF,sGAAsG;IACtG,aAAa,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,GAAG,OAAO;IAQrD,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,cAAc;IAgBtB;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAiCnB;iGAC6F;IAC7F,OAAO,CAAC,YAAY;IAUpB,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,WAAW;IAYnB,UAAU;IAgCV,MAAM,CAAC,EAAE,EAAE,MAAM;IAoDjB;;;;;OAKG;IACH,OAAO,CAAC,UAAU;IAuBlB,OAAO;CAgBR"}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { ADDRESS_CLAMP_TO_EDGE, BLEND_NORMAL, CULLFACE_NONE, Color, FILTER_LINEAR, LAYERID_IMMEDIATE, Mesh, MeshInstance, Quat, Script, StandardMaterial, Texture, Vec3, XRTYPE_VR } from 'playcanvas';
|
|
2
|
+
import { ButtonArmingLatch } from './xr-button-arming';
|
|
3
|
+
import { INITIAL_HOLD_STATE, advanceHold } from './xr-button-hold';
|
|
4
|
+
import { FaceButtonPressTracker, secondaryFaceButtonPressed } from './xr-face-buttons';
|
|
5
|
+
import { drawProgressPie } from './xr-progress-pie';
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
8
|
* Boolean ray-sphere hit test. Treats the ray as a half-line (t >= 0) and returns true when it
|
|
@@ -16,8 +20,8 @@ export const raySphereIntersect = (origin, direction, center, radius) => {
|
|
|
16
20
|
};
|
|
17
21
|
const TEXTURE_SIZE = 256;
|
|
18
22
|
|
|
19
|
-
/**
|
|
20
|
-
const
|
|
23
|
+
/** Seconds of continuous B/Y hold required to leave the session. Matches the gaze-dwell threshold. */
|
|
24
|
+
const EXIT_HOLD_SECONDS = 1.25;
|
|
21
25
|
export class XrExitButton extends Script {
|
|
22
26
|
static scriptName = 'xrExitButton';
|
|
23
27
|
|
|
@@ -33,14 +37,19 @@ export class XrExitButton extends Script {
|
|
|
33
37
|
_headPosition = new Vec3();
|
|
34
38
|
_headRotation = new Quat();
|
|
35
39
|
_worldOffset = new Vec3();
|
|
36
|
-
|
|
40
|
+
_faceButtons = new FaceButtonPressTracker();
|
|
41
|
+
_drawnProgress = -1;
|
|
42
|
+
_hold = INITIAL_HOLD_STATE;
|
|
43
|
+
_arming = new ButtonArmingLatch();
|
|
37
44
|
_isVrActive = () => this.app.xr?.active === true && this.app.xr?.type === XRTYPE_VR;
|
|
38
45
|
_onXrStart = () => {
|
|
39
46
|
this.entity.enabled = this._isVrActive();
|
|
47
|
+
this._resetProgress();
|
|
40
48
|
};
|
|
41
49
|
_onXrEnd = () => {
|
|
42
50
|
this.entity.enabled = false;
|
|
43
51
|
this._hovered = false;
|
|
52
|
+
this._resetProgress();
|
|
44
53
|
this.entity.setLocalScale(1, 1, 1);
|
|
45
54
|
if (this._material) {
|
|
46
55
|
this._material.opacity = 0.9;
|
|
@@ -51,40 +60,25 @@ export class XrExitButton extends Script {
|
|
|
51
60
|
if (!this.entity.enabled) {
|
|
52
61
|
return;
|
|
53
62
|
}
|
|
54
|
-
if (this.
|
|
63
|
+
if (this.rayHitsButton(inputSource.getOrigin(), inputSource.getDirection())) {
|
|
55
64
|
this.app.xr?.end();
|
|
56
65
|
}
|
|
57
66
|
};
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
|
|
68
|
+
/** True when the ray points at the button's hit sphere. False while the button entity is disabled. */
|
|
69
|
+
rayHitsButton(origin, direction) {
|
|
70
|
+
if (!this.entity.enabled) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return raySphereIntersect(origin, direction, this.entity.getPosition(), this.hitRadius);
|
|
64
74
|
}
|
|
65
|
-
|
|
75
|
+
_createCanvas() {
|
|
66
76
|
const canvas = document.createElement('canvas');
|
|
67
77
|
canvas.width = TEXTURE_SIZE;
|
|
68
78
|
canvas.height = TEXTURE_SIZE;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
ctx.clearRect(0, 0, TEXTURE_SIZE, TEXTURE_SIZE);
|
|
73
|
-
ctx.fillStyle = 'rgba(20, 20, 20, 0.75)';
|
|
74
|
-
ctx.beginPath();
|
|
75
|
-
ctx.arc(c, c, c * 0.92, 0, Math.PI * 2);
|
|
76
|
-
ctx.fill();
|
|
77
|
-
const arm = TEXTURE_SIZE * 0.24;
|
|
78
|
-
ctx.strokeStyle = '#ffffff';
|
|
79
|
-
ctx.lineWidth = TEXTURE_SIZE * 0.09;
|
|
80
|
-
ctx.lineCap = 'round';
|
|
81
|
-
ctx.beginPath();
|
|
82
|
-
ctx.moveTo(c - arm, c - arm);
|
|
83
|
-
ctx.lineTo(c + arm, c + arm);
|
|
84
|
-
ctx.moveTo(c + arm, c - arm);
|
|
85
|
-
ctx.lineTo(c - arm, c + arm);
|
|
86
|
-
ctx.stroke();
|
|
87
|
-
}
|
|
79
|
+
return canvas;
|
|
80
|
+
}
|
|
81
|
+
_createTexture(canvas) {
|
|
88
82
|
const texture = new Texture(this.app.graphicsDevice, {
|
|
89
83
|
name: 'xr-exit-button',
|
|
90
84
|
width: TEXTURE_SIZE,
|
|
@@ -98,6 +92,56 @@ export class XrExitButton extends Script {
|
|
|
98
92
|
texture.setSource(canvas);
|
|
99
93
|
return texture;
|
|
100
94
|
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Repaints the button: dark disc, then the hold progress wedge, then the X arms on top so the
|
|
98
|
+
* glyph stays legible over the fill. The pie is skipped entirely at zero progress so the idle
|
|
99
|
+
* button doesn't wear the pie's faint track. Returns whether it painted, so callers can tell a
|
|
100
|
+
* missing context from a successful repaint.
|
|
101
|
+
*/
|
|
102
|
+
_drawButton(progress) {
|
|
103
|
+
const ctx = this._context;
|
|
104
|
+
if (!ctx) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
const c = TEXTURE_SIZE / 2;
|
|
108
|
+
ctx.clearRect(0, 0, TEXTURE_SIZE, TEXTURE_SIZE);
|
|
109
|
+
ctx.fillStyle = 'rgba(20, 20, 20, 0.75)';
|
|
110
|
+
ctx.beginPath();
|
|
111
|
+
ctx.arc(c, c, c * 0.92, 0, Math.PI * 2);
|
|
112
|
+
ctx.fill();
|
|
113
|
+
if (progress > 0) {
|
|
114
|
+
drawProgressPie(ctx, TEXTURE_SIZE, progress);
|
|
115
|
+
}
|
|
116
|
+
const arm = TEXTURE_SIZE * 0.24;
|
|
117
|
+
ctx.strokeStyle = '#ffffff';
|
|
118
|
+
ctx.lineWidth = TEXTURE_SIZE * 0.09;
|
|
119
|
+
ctx.lineCap = 'round';
|
|
120
|
+
ctx.beginPath();
|
|
121
|
+
ctx.moveTo(c - arm, c - arm);
|
|
122
|
+
ctx.lineTo(c + arm, c + arm);
|
|
123
|
+
ctx.moveTo(c + arm, c - arm);
|
|
124
|
+
ctx.lineTo(c - arm, c + arm);
|
|
125
|
+
ctx.stroke();
|
|
126
|
+
this._texture?.upload();
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Repaints only when the drawn wedge would visibly move. Only records the paint on success, so a
|
|
131
|
+
* missing context doesn't fool the dedupe cache into thinking a repaint already happened. */
|
|
132
|
+
_setProgress(progress) {
|
|
133
|
+
if (Math.abs(progress - this._drawnProgress) <= 0.001) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (this._drawButton(progress)) {
|
|
137
|
+
this._drawnProgress = progress;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
_resetProgress() {
|
|
141
|
+
this._hold = INITIAL_HOLD_STATE;
|
|
142
|
+
this._drawnProgress = -1;
|
|
143
|
+
this._setProgress(0);
|
|
144
|
+
}
|
|
101
145
|
_createMesh() {
|
|
102
146
|
const h = this.halfSize;
|
|
103
147
|
const mesh = new Mesh(this.app.graphicsDevice);
|
|
@@ -109,7 +153,10 @@ export class XrExitButton extends Script {
|
|
|
109
153
|
return mesh;
|
|
110
154
|
}
|
|
111
155
|
initialize() {
|
|
112
|
-
this.
|
|
156
|
+
this._canvas = this._createCanvas();
|
|
157
|
+
this._context = this._canvas.getContext('2d') ?? undefined;
|
|
158
|
+
this._texture = this._createTexture(this._canvas);
|
|
159
|
+
this._resetProgress();
|
|
113
160
|
const material = new StandardMaterial();
|
|
114
161
|
material.useLighting = false;
|
|
115
162
|
material.emissive = new Color(1, 1, 1);
|
|
@@ -136,26 +183,38 @@ export class XrExitButton extends Script {
|
|
|
136
183
|
this.app.xr?.on('end', this._onXrEnd);
|
|
137
184
|
this.app.xr?.input?.on('select', this._onSelect);
|
|
138
185
|
}
|
|
139
|
-
update() {
|
|
186
|
+
update(dt) {
|
|
140
187
|
if (!this.entity.enabled) {
|
|
141
188
|
return;
|
|
142
189
|
}
|
|
143
190
|
this._trackHead();
|
|
144
191
|
const sources = this.app.xr?.input?.inputSources ?? [];
|
|
145
192
|
let hovered = false;
|
|
193
|
+
let holdActive = false;
|
|
146
194
|
for (const source of sources) {
|
|
147
|
-
const sourceHovers = this.
|
|
195
|
+
const sourceHovers = this.rayHitsButton(source.getOrigin(), source.getDirection());
|
|
148
196
|
hovered = hovered || sourceHovers;
|
|
149
197
|
|
|
150
198
|
// A face-button press exits only while that controller is aimed at the button, matching the
|
|
151
199
|
// trigger. Edge-detected so holding a button and then aiming onto it doesn't fire.
|
|
152
|
-
const facePressed = this.
|
|
153
|
-
|
|
154
|
-
this._faceButtonDown.set(source, facePressed);
|
|
155
|
-
if (sourceHovers && facePressed && !faceWasDown) {
|
|
200
|
+
const facePressed = this._faceButtons.justPressed(source);
|
|
201
|
+
if (sourceHovers && facePressed) {
|
|
156
202
|
this.app.xr?.end();
|
|
157
203
|
return;
|
|
158
204
|
}
|
|
205
|
+
|
|
206
|
+
// Two statements, not `holdActive = holdActive || this._arming.trackPress(...)`: trackPress
|
|
207
|
+
// arms the source as a side effect and must run for every source every frame, but `||`
|
|
208
|
+
// short-circuits once holdActive is true and would skip arming later sources.
|
|
209
|
+
const armedPress = this._arming.trackPress(source, secondaryFaceButtonPressed(source));
|
|
210
|
+
holdActive = holdActive || armedPress;
|
|
211
|
+
}
|
|
212
|
+
const hold = advanceHold(this._hold, holdActive, dt, EXIT_HOLD_SECONDS);
|
|
213
|
+
this._hold = hold.state;
|
|
214
|
+
this._setProgress(hold.progress);
|
|
215
|
+
if (hold.justFired) {
|
|
216
|
+
this.app.xr?.end();
|
|
217
|
+
return;
|
|
159
218
|
}
|
|
160
219
|
if (hovered !== this._hovered) {
|
|
161
220
|
this._hovered = hovered;
|
|
@@ -203,5 +262,7 @@ export class XrExitButton extends Script {
|
|
|
203
262
|
this._mesh = undefined;
|
|
204
263
|
this._material = undefined;
|
|
205
264
|
this._texture = undefined;
|
|
265
|
+
this._canvas = undefined;
|
|
266
|
+
this._context = undefined;
|
|
206
267
|
}
|
|
207
268
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { XrInputSource } from 'playcanvas';
|
|
2
|
+
/** True while either face button is held. False for hand-tracked sources, which have no gamepad. */
|
|
3
|
+
export declare const faceButtonPressed: (inputSource: XrInputSource) => boolean;
|
|
4
|
+
/** True while B or Y is held, regardless of which hand holds the controller. */
|
|
5
|
+
export declare const secondaryFaceButtonPressed: (inputSource: XrInputSource) => boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Edge-detects face-button presses per input source so one press fires once. Callers must poll every
|
|
8
|
+
* frame for every source they care about; a press and release between polls is missed.
|
|
9
|
+
*/
|
|
10
|
+
export declare class FaceButtonPressTracker {
|
|
11
|
+
private _down;
|
|
12
|
+
/** True only on the poll where a face button goes from released to pressed. */
|
|
13
|
+
justPressed(inputSource: XrInputSource): boolean;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=xr-face-buttons.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xr-face-buttons.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/xr-face-buttons.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAc3C,oGAAoG;AACpG,eAAO,MAAM,iBAAiB,GAAI,aAAa,aAAa,KAAG,OACX,CAAC;AAErD,gFAAgF;AAChF,eAAO,MAAM,0BAA0B,GAAI,aAAa,aAAa,KAAG,OACV,CAAC;AAE/D;;;GAGG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,KAAK,CAAyC;IAEtD,+EAA+E;IAC/E,WAAW,CAAC,WAAW,EAAE,aAAa,GAAG,OAAO;CAOjD"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** xr-standard gamepad button indices for the face buttons: A/X (4) and B/Y (5). */
|
|
2
|
+
const FACE_BUTTON_INDICES = [4, 5];
|
|
3
|
+
|
|
4
|
+
/** xr-standard gamepad button indices for the secondary face button: B on the right hand, Y on the left. */
|
|
5
|
+
const SECONDARY_FACE_BUTTON_INDICES = [5];
|
|
6
|
+
const anyButtonPressed = (inputSource, indices) => {
|
|
7
|
+
const buttons = inputSource.gamepad?.buttons;
|
|
8
|
+
return buttons ? indices.some(index => buttons[index]?.pressed === true) : false;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** True while either face button is held. False for hand-tracked sources, which have no gamepad. */
|
|
12
|
+
export const faceButtonPressed = inputSource => anyButtonPressed(inputSource, FACE_BUTTON_INDICES);
|
|
13
|
+
|
|
14
|
+
/** True while B or Y is held, regardless of which hand holds the controller. */
|
|
15
|
+
export const secondaryFaceButtonPressed = inputSource => anyButtonPressed(inputSource, SECONDARY_FACE_BUTTON_INDICES);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Edge-detects face-button presses per input source so one press fires once. Callers must poll every
|
|
19
|
+
* frame for every source they care about; a press and release between polls is missed.
|
|
20
|
+
*/
|
|
21
|
+
export class FaceButtonPressTracker {
|
|
22
|
+
_down = new WeakMap();
|
|
23
|
+
|
|
24
|
+
/** True only on the poll where a face button goes from released to pressed. */
|
|
25
|
+
justPressed(inputSource) {
|
|
26
|
+
const pressed = faceButtonPressed(inputSource);
|
|
27
|
+
const wasPressed = this._down.get(inputSource) ?? false;
|
|
28
|
+
this._down.set(inputSource, pressed);
|
|
29
|
+
return pressed && !wasPressed;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { FaceButtonPressTracker, faceButtonPressed, secondaryFaceButtonPressed } from './xr-face-buttons';
|
|
2
|
+
|
|
3
|
+
/** Fake input source whose gamepad reports `pressedIndices` as held. */
|
|
4
|
+
const source = (pressedIndices, buttonCount = 6) => ({
|
|
5
|
+
gamepad: {
|
|
6
|
+
buttons: Array.from({
|
|
7
|
+
length: buttonCount
|
|
8
|
+
}, (_, index) => ({
|
|
9
|
+
pressed: pressedIndices.includes(index)
|
|
10
|
+
}))
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
const handTrackedSource = () => ({});
|
|
14
|
+
describe('faceButtonPressed', () => {
|
|
15
|
+
it('is false for an input source with no gamepad', () => {
|
|
16
|
+
expect(faceButtonPressed(handTrackedSource())).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
it('is false when the gamepad has fewer buttons than the face buttons', () => {
|
|
19
|
+
expect(faceButtonPressed(source([0], 2))).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
it('is true when A/X is pressed', () => {
|
|
22
|
+
expect(faceButtonPressed(source([4]))).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
it('is true when B/Y is pressed', () => {
|
|
25
|
+
expect(faceButtonPressed(source([5]))).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
it('is false when only the trigger and grip are pressed', () => {
|
|
28
|
+
expect(faceButtonPressed(source([0, 1]))).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
it('is false when nothing is pressed', () => {
|
|
31
|
+
expect(faceButtonPressed(source([]))).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe('secondaryFaceButtonPressed', () => {
|
|
35
|
+
it('is true when B/Y is pressed', () => {
|
|
36
|
+
expect(secondaryFaceButtonPressed(source([5]))).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it('is false when only A/X is pressed', () => {
|
|
39
|
+
expect(secondaryFaceButtonPressed(source([4]))).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
it('is false for an input source with no gamepad', () => {
|
|
42
|
+
expect(secondaryFaceButtonPressed(handTrackedSource())).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it('is false when the gamepad has fewer buttons than the face buttons', () => {
|
|
45
|
+
expect(secondaryFaceButtonPressed(source([0], 2))).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('FaceButtonPressTracker', () => {
|
|
49
|
+
it('fires once for a held press and re-arms after release', () => {
|
|
50
|
+
const tracker = new FaceButtonPressTracker();
|
|
51
|
+
// One persistent source whose button state mutates, as a real XrInputSource does across frames.
|
|
52
|
+
const buttons = Array.from({
|
|
53
|
+
length: 6
|
|
54
|
+
}, () => ({
|
|
55
|
+
pressed: false
|
|
56
|
+
}));
|
|
57
|
+
const controller = {
|
|
58
|
+
gamepad: {
|
|
59
|
+
buttons
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const holdFaceButton = pressed => {
|
|
63
|
+
buttons[4].pressed = pressed;
|
|
64
|
+
};
|
|
65
|
+
expect(tracker.justPressed(controller)).toBe(false);
|
|
66
|
+
holdFaceButton(true);
|
|
67
|
+
expect(tracker.justPressed(controller)).toBe(true);
|
|
68
|
+
expect(tracker.justPressed(controller)).toBe(false);
|
|
69
|
+
holdFaceButton(false);
|
|
70
|
+
expect(tracker.justPressed(controller)).toBe(false);
|
|
71
|
+
holdFaceButton(true);
|
|
72
|
+
expect(tracker.justPressed(controller)).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
it('tracks each input source independently', () => {
|
|
75
|
+
const tracker = new FaceButtonPressTracker();
|
|
76
|
+
const left = source([4]);
|
|
77
|
+
const right = source([]);
|
|
78
|
+
expect(tracker.justPressed(left)).toBe(true);
|
|
79
|
+
expect(tracker.justPressed(right)).toBe(false);
|
|
80
|
+
expect(tracker.justPressed(left)).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface DwellState {
|
|
2
|
+
targetIndex: number | null;
|
|
3
|
+
elapsed: number;
|
|
4
|
+
}
|
|
5
|
+
export interface DwellUpdate {
|
|
6
|
+
state: DwellState;
|
|
7
|
+
progress: number;
|
|
8
|
+
justOpened: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const INITIAL_DWELL_STATE: DwellState;
|
|
11
|
+
/**
|
|
12
|
+
* Advances gaze-dwell timing for one frame. `target` is the annotation index currently under the
|
|
13
|
+
* gaze ray, or null. Dwell time accumulates while the same target is held and resets when it
|
|
14
|
+
* changes or is lost. `justOpened` is true only on the frame the dwell first reaches `threshold`.
|
|
15
|
+
*/
|
|
16
|
+
export declare const advanceDwell: (prev: DwellState, target: number | null, dt: number, threshold: number) => DwellUpdate;
|
|
17
|
+
//# sourceMappingURL=xr-gaze-dwell-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xr-gaze-dwell-state.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/xr-gaze-dwell-state.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,eAAO,MAAM,mBAAmB,EAAE,UAA8C,CAAC;AAEjF;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,UAAU,EAAE,QAAQ,MAAM,GAAG,IAAI,EAAE,IAAI,MAAM,EAAE,WAAW,MAAM,KAAG,WAYrG,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const INITIAL_DWELL_STATE = {
|
|
2
|
+
targetIndex: null,
|
|
3
|
+
elapsed: 0
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Advances gaze-dwell timing for one frame. `target` is the annotation index currently under the
|
|
8
|
+
* gaze ray, or null. Dwell time accumulates while the same target is held and resets when it
|
|
9
|
+
* changes or is lost. `justOpened` is true only on the frame the dwell first reaches `threshold`.
|
|
10
|
+
*/
|
|
11
|
+
export const advanceDwell = (prev, target, dt, threshold) => {
|
|
12
|
+
if (target === null) {
|
|
13
|
+
return {
|
|
14
|
+
state: INITIAL_DWELL_STATE,
|
|
15
|
+
progress: 0,
|
|
16
|
+
justOpened: false
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (target !== prev.targetIndex) {
|
|
20
|
+
return {
|
|
21
|
+
state: {
|
|
22
|
+
targetIndex: target,
|
|
23
|
+
elapsed: 0
|
|
24
|
+
},
|
|
25
|
+
progress: 0,
|
|
26
|
+
justOpened: false
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const elapsed = Math.min(prev.elapsed + dt, threshold);
|
|
30
|
+
const justOpened = prev.elapsed < threshold && elapsed >= threshold;
|
|
31
|
+
return {
|
|
32
|
+
state: {
|
|
33
|
+
targetIndex: target,
|
|
34
|
+
elapsed
|
|
35
|
+
},
|
|
36
|
+
progress: elapsed / threshold,
|
|
37
|
+
justOpened
|
|
38
|
+
};
|
|
39
|
+
};
|