_normalizePointerEvent(e) {
    // All pointer events in a 3D world will be given a `ray` property.
    if (!e.ray) {
      // normalize touch events
      let posInfo = e
      if (e.touches) {
        let touches = /^touch(end|cancel)$/.test(e.type) ? e.changedTouches : e.touches
        if (touches.length === 1) {
          posInfo = touches[0]
        }
      }

      // convert mouse position to normalized device coords (-1 to 1)
      const canvasRect = e.target.getBoundingClientRect() //e.target is the canvas
      let width = canvasRect.width || this.width //use logical size if no visible rect, e.g. offscreen canvas
      let height = canvasRect.height || this.height
      let u = ((posInfo.clientX || 0) - (canvasRect.left || 0)) / width * 2 - 1
      let v = ((posInfo.clientY || 0) - (canvasRect.top || 0)) / height * -2 + 1

      // ensure camera's matrix is up to date
      let camera = this.getChildByKey('camera')
      camera.updateMatrices()

      // calculate the ray and put it on the event
      e.ray = camera.getRayAtProjectedCoords(u, v)
    }

    super._normalizePointerEvent(e)
  }
@override
In 3D worlds, we will normalize all pointer events so they always carry a `ray` property;
handlers for these events should then only rely on that, which is guaranteed to be present,
unlike `clientX/Y` etc. which are only present for pointer events originating from a screen.