All files / src/components/VirtualWalkthrough xr-annotation-candidate-buffer.ts

100% Statements 35/35
100% Branches 17/17
100% Functions 3/3
100% Lines 34/34

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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98      2x                       2x             2x 30x 13x 6x       24x                         22x 22x                       30x 24x     30x 30x 6x   6x     24x 24x 29x 1x     28x 28x 2x     26x 26x 26x   26x 6x 6x 6x 6x   20x   26x     24x   24x      
import { Vec3 } from 'playcanvas';
 
/** Local half-extent of the unit-plane hotspot quad. */
export const HOTSPOT_HALF_EXTENT = 0.5;
 
export interface AnnotationCandidate {
  entity: any;
  script: any;
  position: Vec3;
  radius: number;
}
 
/** Pulls the stock annotation script off a hotspot entity, or undefined if it carries none. */
export type AnnotationScriptLookup = (entity: any) => any;
 
const scratchScale = new Vec3();
 
/**
 * Whether `node` still hangs beneath `sceneRoot`. An entity that has been removed from the scene
 * keeps its own `children` array - a destroyed one keeps it as an empty array - so only a walk back
 * up the parent chain tells a live root apart from a detached one.
 */
const isInScene = (node: any, sceneRoot: any): boolean => {
  for (let current = node; current; current = current.parent) {
    if (current === sceneRoot) {
      return true;
    }
  }
 
  return false;
};
 
/**
 * Collects world-space hit candidates for the openable annotation hotspots into a buffer it owns and
 * reuses, so a caller running every frame allocates nothing. The returned array and the candidate
 * objects inside it are overwritten by the next `collect`, which is why each caller holds its own
 * buffer rather than sharing one.
 *
 * Positions and radii are re-read on every call, so hotspots that move or resize stay targetable.
 * That read is cheap; it is the allocation around it that a per-frame caller cannot afford.
 */
export class AnnotationCandidateBuffer {
  private readonly _candidates: AnnotationCandidate[] = [];
  private _root: any = null;
 
  constructor(private readonly _getScript: AnnotationScriptLookup) {}
 
  /**
   * Hit candidates for every openable hotspot except `excludeName`, with hit spheres derived from
   * each hotspot's rendered world size and padded by `radiusPad` for easier aiming.
   */
  collect(app: any, radiusPad: number, excludeName?: string): AnnotationCandidate[] {
    // The root is looked up by a walk of the whole scene graph, so it is held onto between frames.
    // It is re-resolved whenever it leaves the scene: annotations mount after the scene does, and
    // the root is torn down and built afresh every time the annotation list empties and refills.
    if (!isInScene(this._root, app.root)) {
      this._root = app.root.findByName('annotations-root');
    }
 
    const children = this._root?.children;
    if (!children) {
      this._candidates.length = 0;
 
      return this._candidates;
    }
 
    let count = 0;
    for (const entity of children) {
      if (excludeName !== undefined && entity.name === excludeName) {
        continue;
      }
 
      const script = this._getScript(entity);
      if (!script || script.enabled === false || typeof script.onVrOpenCallback !== 'function') {
        continue;
      }
 
      entity.getWorldTransform().getScale(scratchScale);
      const radius = HOTSPOT_HALF_EXTENT * scratchScale.x * radiusPad;
      const existing = this._candidates[count];
 
      if (existing) {
        existing.entity = entity;
        existing.script = script;
        existing.position = entity.getPosition();
        existing.radius = radius;
      } else {
        this._candidates.push({ entity, script, position: entity.getPosition(), radius });
      }
      count++;
    }
 
    this._candidates.length = count;
 
    return this._candidates;
  }
}