All files / src/components/VirtualWalkthrough xr-annotation-interaction.ts

0% Statements 0/31
0% Branches 0/20
0% Functions 0/7
0% Lines 0/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72                                                                                                                                               
import { CameraComponent, Script, Vec3, XRTYPE_VR, XrInputSource } from 'playcanvas';
 
import { HIT_RADIUS_PAD, collectAnnotationHitCandidates } from './xr-annotation-candidates';
import { nearestAnnotationHit } from './xr-annotation-targeting';
import { FaceButtonPressTracker } from './xr-face-buttons';
import { rayHitsAnnotationPanel } from './xr-interactive-ui';
 
export class XrAnnotationInteraction extends Script {
  static scriptName = 'xrAnnotationInteraction';
 
  onDismissCallback?: () => void;
 
  private _faceButtons = new FaceButtonPressTracker();
 
  private _isVrActive = () => this.app.xr?.active === true && this.app.xr?.type === XRTYPE_VR;
 
  private _dismiss = () => {
    if (typeof this.onDismissCallback === 'function') {
      this.onDismissCallback();
    }
  };
 
  private _onSelect = (inputSource: XrInputSource) => {
    if (!this._isVrActive()) {
      return;
    }
    this._openAnnotationUnderRay(inputSource.getOrigin(), inputSource.getDirection());
  };
 
  private _openAnnotationUnderRay(origin: Vec3, direction: Vec3) {
    // Checked before the hotspots so aiming at the panel (e.g. its carousel arrows) neither
    // dismisses it nor opens an annotation whose padded hit sphere sits behind it.
    if (rayHitsAnnotationPanel(this.app, origin, direction)) {
      return;
    }
 
    const candidates = collectAnnotationHitCandidates(this.app, HIT_RADIUS_PAD);
    const index = nearestAnnotationHit(origin, direction, candidates);
    if (index === null) {
      this._dismiss();
 
      return;
    }
 
    const { entity, script } = candidates[index];
    const camera = this.app.root.findComponent('camera') as CameraComponent | null;
    const screen = camera?.worldToScreen(entity.getPosition());
    script.onVrOpenCallback(screen?.x ?? 0, screen?.y ?? 0);
  }
 
  initialize() {
    this.app.xr?.input?.on('select', this._onSelect);
    this.once('destroy', () => this.app.xr?.input?.off('select', this._onSelect));
  }
 
  update() {
    if (!this._isVrActive()) {
      return;
    }
 
    // A face button dismisses from any aim direction, unlike the click-out select. Harmless when
    // nothing is open: the React close handler re-sets state it already holds.
    for (const inputSource of this.app.xr?.input?.inputSources ?? []) {
      if (this._faceButtons.justPressed(inputSource)) {
        this._dismiss();
 
        return;
      }
    }
  }
}